@primeuicom/mcp 0.1.10 → 0.1.11

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/service.js CHANGED
@@ -43,6 +43,36 @@ var exportItemSchema = z.object({
43
43
  status: exportStatusSchema,
44
44
  createdAt: z.string().describe("Export creation timestamp in ISO-8601 UTC format.")
45
45
  }).describe("Single export record from PrimeUI export history.");
46
+ var fileTransferSchema = z.object({
47
+ sourcePath: z.string().describe("Relative source file path inside downloaded export root."),
48
+ targetPath: z.string().describe("Relative target file path inside user project root.")
49
+ });
50
+ var conflictFileSchema = fileTransferSchema.extend({
51
+ diff: z.string().describe(
52
+ "Unified diff between user file and export file. For binary files the diff contains a plain message."
53
+ ),
54
+ isBinary: z.boolean().describe("True if conflict comparison was treated as binary data.")
55
+ });
56
+ var dependencySectionSchema = z.enum([
57
+ "dependencies",
58
+ "devDependencies",
59
+ "peerDependencies"
60
+ ]);
61
+ var dependencyToAddSchema = z.object({
62
+ packageName: z.string().describe("Dependency package name that was missing in user package.json."),
63
+ version: z.string().describe("Version from exported project's package.json."),
64
+ section: dependencySectionSchema.describe(
65
+ "package.json section where dependency should be added."
66
+ )
67
+ });
68
+ var dependencyVersionConflictSchema = z.object({
69
+ packageName: z.string().describe("Dependency package name with version mismatch."),
70
+ section: dependencySectionSchema.describe(
71
+ "Section where export expects this dependency."
72
+ ),
73
+ exportVersion: z.string().describe("Version found in exported project's package.json."),
74
+ userVersion: z.string().describe("Version currently present in user's package.json.")
75
+ });
46
76
  var TRIGGER_PHRASES = [
47
77
  "import page from PrimeUI",
48
78
  "add page from PrimeUI",
@@ -59,8 +89,8 @@ WORKFLOW ORDER (always follow this sequence):
59
89
  2. Reconcile PrimeUI pages with local project pages/routes/paths, then confirm target pages with user
60
90
  3. primeui_create_export -> generate export snapshot for agreed pages/analysis scope
61
91
  4. primeui_download_export -> download to temp directory
62
- 5. Reconcile downloaded export pages with local files, resolve ambiguities
63
- 6. Move agreed page code + dependencies into user's project and apply integration updates where appropriate
92
+ 5. primeui_copy_page -> copy one page safely (repeat for each selected page)
93
+ 6. Reconcile integration points and resolve reported conflicts, if any
64
94
  7. primeui_clear_temp -> cleanup temp files
65
95
  `.trim();
66
96
  var initialInstructions = `
@@ -82,16 +112,16 @@ CRITICAL RULES FOR PAGE IMPORT:
82
112
  - Before creating export, ask user which pages to add/update (specific pages or all missing pages) and keep that choice in thread context.
83
113
  - The downloaded export in .primeui/temp/ contains a full standalone Next.js project.
84
114
  Do NOT copy it wholesale.
85
- - Copy ONLY the target page file and its direct component dependencies.
86
- - Always use pagePath and componentsPath from tool responses as source of truth for file locations.
87
- - Trace each page's imports to identify which component folders are needed.
88
- - Check if components already exist in user's project - merge carefully, don't blindly overwrite.
89
- - Adjust import paths to match user's project structure.
90
- - After download/import work, inspect integration points (navigation configs, link menus, route registries, page indexes).
115
+ - Always call primeui_copy_page for each confirmed page instead of manual file copy.
116
+ - primeui_copy_page performs safe copy only:
117
+ - new files are copied,
118
+ - identical files are reported,
119
+ - conflicting files are NEVER overwritten and are returned with diff.
120
+ - primeui_copy_page may add only missing dependencies to user's package.json.
121
+ It never upgrades existing dependency versions and never runs dependency installation.
122
+ - After page copy operations, inspect integration points (navigation configs, link menus, route registries, page indexes).
91
123
  - If integration pattern is obvious, apply it and explicitly mark it in the report with "\u26A0 integration update".
92
124
  - If integration pattern is unclear, ask the user before editing integration files.
93
- - Do NOT copy: package.json, next.config, tailwind.config, layout files, or any project-level config.
94
- - If merge conflicts arise and cannot be resolved unambiguously, STOP and ask the user to decide.
95
125
  `.trim();
96
126
  var toolGetProjectInfo = {
97
127
  title: "PrimeUI Project Info",
@@ -200,30 +230,19 @@ var toolDownloadExport = {
200
230
 
201
231
  Do NOT call this as the first step. Requires export ID from primeui_create_export. Start with primeui_get_project_info instead.
202
232
 
203
- WHEN TO USE: Call this AFTER primeui_create_export has returned a completed export. Requires the export ID returned by primeui_create_export (or from primeui_list_exports if the user explicitly requested a previous export).
233
+ WHEN TO USE: Call this AFTER primeui_create_export has returned a completed export. Requires export ID from primeui_create_export so MCP can use the matching local pages snapshot for sidecar manifest generation.
234
+
235
+ NOTE:
236
+ - Download of an export ID without local pages snapshot may fail.
237
+ - For previous exports, create a fresh export first when possible.
204
238
 
205
239
  AFTER DOWNLOAD:
206
- - Reconcile downloaded export pages with the current local project state again.
207
- - If thread context already defines which pages to add/update and there is no ambiguity, complete the transfer without extra confirmation.
208
- - If ambiguity remains (especially page/path mismatches), analyze differences in detail and ask direct clarification questions before editing.
209
- - The response includes pagePath and componentsPath fields. Use these as direct pointers to locate page code and dependencies in export files.
210
-
211
- MANUAL IMPORT STEPS per page (do not skip):
212
- 1. Read the page file at the pagePath from the response.
213
- 2. Read the component files at the componentsPath from the response. Trace the page's imports to identify which specific component folders are needed.
214
- 3. Copy ONLY the page file and its component dependencies into the user's project.
215
- 4. Adjust all import paths to match the user's project structure.
216
- 5. Check for existing components in user's project:
217
- - If code overlaps or was modified on the user's side, carefully determine the nature of changes on both sides.
218
- - If the merge conflict can be resolved unambiguously - resolve it.
219
- - If NOT - STOP, explain the conflict to the user, and ask for their decision before continuing.
220
- 6. Inspect integration points used by existing pages (navigation configs, route registries, link components, page indexes).
221
- 7. If integration pattern is obvious and consistent with project conventions, apply integration updates and mark them in the report as "\u26A0 integration update".
222
- 8. If integration pattern is not obvious, provide concrete integration suggestions and ask user confirmation before changing integration files.
223
- 9. Do NOT copy project config files (package.json, next.config, tailwind.config, layout, etc.).
224
- 10. REPEAT steps 1-9 for each page the user wants to import.
225
- 11. Report exactly which files/areas were changed and which items require user follow-up.
226
- 12. Only after ALL pages are successfully imported, call primeui_clear_temp to clean up.
240
+ - Use manifestPath from this response as the contract for page slug/path mapping in copy operations.
241
+ - For each selected page, call primeui_copy_page with:
242
+ - originPageSlug (required),
243
+ - actualPageSlug (optional, if route remap is needed).
244
+ - If there are unresolved conflicts in copy report, stop and ask the user.
245
+ - Only after all requested pages are copied and reconciled, call primeui_clear_temp.
227
246
 
228
247
  ${WORKFLOW_SUMMARY}`,
229
248
  inputSchema: {
@@ -238,6 +257,9 @@ MANUAL IMPORT STEPS per page (do not skip):
238
257
  projectPath: z.string().describe(
239
258
  "Absolute local path to extracted export root in .primeui/temp/exports/[exportId]. Read files from here only."
240
259
  ),
260
+ manifestPath: z.string().describe(
261
+ "Absolute path to sidecar export manifest file (.primeui/temp/exports/[exportId].manifest.json). primeui_copy_page depends on this file."
262
+ ),
241
263
  pages: z.array(pageSchema).describe(
242
264
  "Page descriptors for import decisions. Use pagePath/componentsPath from each item when copying code into user project."
243
265
  )
@@ -249,6 +271,77 @@ MANUAL IMPORT STEPS per page (do not skip):
249
271
  openWorldHint: true
250
272
  }
251
273
  };
274
+ var toolCopyPage = {
275
+ title: "PrimeUI Copy Page",
276
+ description: `Safely copy one page from downloaded export into the user's local project. Works only with local files in .primeui/temp/exports and does NOT call PrimeUI API.
277
+
278
+ WHEN TO USE:
279
+ - Call this AFTER primeui_download_export.
280
+ - Call once per selected page.
281
+ - Use actualPageSlug only when user wants to remap route during import.
282
+
283
+ BEHAVIOR:
284
+ - Reads export sidecar manifest to resolve originPageSlug -> pagePath/componentsPath.
285
+ - Copies page file + full page components folder + recursively discovered internal dependencies.
286
+ - Never overwrites existing conflicting files.
287
+ - Reports:
288
+ a) copied new files,
289
+ b) identical existing files,
290
+ c) conflicting files with diff,
291
+ d) missing dependencies added to package.json,
292
+ e) dependency version conflicts (report-only, no auto upgrade).
293
+ - Adds only missing dependencies to package.json (dependencies/devDependencies/peerDependencies).
294
+ - Never updates existing dependency versions and never runs install commands.
295
+
296
+ ${WORKFLOW_SUMMARY}`,
297
+ inputSchema: {
298
+ originPageSlug: z.string().describe(
299
+ "Source page slug from PrimeUI project pages list, for example '/', '/pricing', '/docs'."
300
+ ),
301
+ actualPageSlug: z.string().optional().describe(
302
+ "Optional destination slug in user's project. If omitted, originPageSlug is used."
303
+ )
304
+ },
305
+ outputSchema: {
306
+ exportId: z.string().describe("Resolved local export identifier used for copy operation."),
307
+ exportPath: z.string().describe("Absolute path to local extracted export project root."),
308
+ originPageSlug: z.string().describe("Normalized source page slug requested for copy."),
309
+ actualPageSlug: z.string().describe("Normalized destination slug used for target route paths."),
310
+ sourcePagePath: z.string().describe("Source page path relative to export root."),
311
+ targetPagePath: z.string().describe("Destination page path relative to user project root."),
312
+ sourceComponentsPath: z.string().describe("Source components directory relative to export root."),
313
+ targetComponentsPath: z.string().describe(
314
+ "Destination components directory relative to user project root."
315
+ ),
316
+ newFiles: z.array(fileTransferSchema).describe("New files copied into user project without conflicts."),
317
+ identicalFiles: z.array(fileTransferSchema).describe(
318
+ "Existing files in user project that are byte-identical to export files."
319
+ ),
320
+ conflictFiles: z.array(conflictFileSchema).describe(
321
+ "Files that already exist and differ from export version. Never overwritten."
322
+ ),
323
+ addedDependencies: z.array(dependencyToAddSchema).describe(
324
+ "Dependencies that were missing and have been added to user's package.json."
325
+ ),
326
+ dependenciesVersionConflicts: z.array(dependencyVersionConflictSchema).describe(
327
+ "Dependencies with version mismatch between export and user project. Report only."
328
+ ),
329
+ summary: z.object({
330
+ totalCandidateFiles: z.number().describe("Total number of candidate files considered for page copy."),
331
+ copiedFiles: z.number().describe("Number of files copied as new."),
332
+ identicalFiles: z.number().describe("Number of files detected as identical."),
333
+ conflictFiles: z.number().describe("Number of conflicting files not copied."),
334
+ addedDependencies: z.number().describe("Count of dependencies added to package.json."),
335
+ dependenciesVersionConflicts: z.number().describe("Count of dependency version mismatches reported.")
336
+ })
337
+ },
338
+ annotations: {
339
+ readOnlyHint: false,
340
+ destructiveHint: false,
341
+ idempotentHint: false,
342
+ openWorldHint: false
343
+ }
344
+ };
252
345
  var toolClearTemp = {
253
346
  title: "PrimeUI Clear Temp",
254
347
  description: `Delete all files in .primeui/temp/ and recreate empty temp structure.
@@ -358,11 +451,27 @@ function createPrimeUiMcpServer(source) {
358
451
  }
359
452
  }
360
453
  );
454
+ server.registerTool(
455
+ "primeui_copy_page",
456
+ toolCopyPage,
457
+ async ({ originPageSlug, actualPageSlug }) => {
458
+ try {
459
+ const result = await source.copyPage(
460
+ originPageSlug,
461
+ actualPageSlug
462
+ );
463
+ return okResult("primeui_copy_page", result);
464
+ } catch (error) {
465
+ return errorResult(error);
466
+ }
467
+ }
468
+ );
361
469
  return server;
362
470
  }
363
471
 
364
472
  // src/services/project-sync-service.ts
365
- import path2 from "path";
473
+ import path4 from "path";
474
+ import { readFile as readFile3 } from "fs/promises";
366
475
 
367
476
  // src/lib/fs.ts
368
477
  import { mkdir, rm, writeFile } from "fs/promises";
@@ -389,7 +498,875 @@ async function extractZip(zipPath, targetDir) {
389
498
  }
390
499
  }
391
500
 
501
+ // src/services/page-copy-service.ts
502
+ import { readFile as readFile2, readdir, stat as stat2, writeFile as writeFile2 } from "fs/promises";
503
+ import path3 from "path";
504
+
505
+ // src/lib/import-graph.ts
506
+ import { readFile, stat } from "fs/promises";
507
+ import { builtinModules } from "module";
508
+ import path2 from "path";
509
+ var INTERNAL_EXTENSIONS = [
510
+ ".ts",
511
+ ".tsx",
512
+ ".js",
513
+ ".jsx",
514
+ ".mjs",
515
+ ".cjs",
516
+ ".json"
517
+ ];
518
+ var PARSEABLE_EXTENSIONS = /* @__PURE__ */ new Set([
519
+ ".ts",
520
+ ".tsx",
521
+ ".js",
522
+ ".jsx",
523
+ ".mjs",
524
+ ".cjs"
525
+ ]);
526
+ var BUILTIN_MODULES = /* @__PURE__ */ new Set([
527
+ ...builtinModules,
528
+ ...builtinModules.map((item) => `node:${item}`)
529
+ ]);
530
+ var STATIC_IMPORT_RE = /\bimport\s+(?:type\s+)?(?:[\s\S]*?\sfrom\s+)?["']([^"']+)["']/g;
531
+ var EXPORT_FROM_RE = /\bexport\s+(?:[\s\S]*?\sfrom\s+)["']([^"']+)["']/g;
532
+ var DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
533
+ var REQUIRE_RE = /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g;
534
+ function collectSpecifiers(content) {
535
+ const results = /* @__PURE__ */ new Set();
536
+ let match = null;
537
+ for (const re of [
538
+ STATIC_IMPORT_RE,
539
+ EXPORT_FROM_RE,
540
+ DYNAMIC_IMPORT_RE,
541
+ REQUIRE_RE
542
+ ]) {
543
+ re.lastIndex = 0;
544
+ while ((match = re.exec(content)) !== null) {
545
+ const specifier = match[1]?.trim();
546
+ if (specifier) {
547
+ results.add(specifier);
548
+ }
549
+ }
550
+ }
551
+ return [...results];
552
+ }
553
+ function getExternalPackageName(specifier) {
554
+ if (!specifier || specifier.startsWith(".") || specifier.startsWith("/")) {
555
+ return null;
556
+ }
557
+ if (specifier.startsWith("node:")) {
558
+ return null;
559
+ }
560
+ if (specifier.startsWith("@/") || specifier.startsWith("@root/")) {
561
+ return null;
562
+ }
563
+ const parts = specifier.split("/");
564
+ if (parts.length === 0) {
565
+ return null;
566
+ }
567
+ if (specifier.startsWith("@")) {
568
+ if (parts.length < 2) {
569
+ return null;
570
+ }
571
+ return `${parts[0]}/${parts[1]}`;
572
+ }
573
+ return parts[0] ?? null;
574
+ }
575
+ async function pathExists(filePath) {
576
+ try {
577
+ await stat(filePath);
578
+ return true;
579
+ } catch {
580
+ return false;
581
+ }
582
+ }
583
+ async function resolveFileCandidate(candidateBase) {
584
+ const ext = path2.extname(candidateBase);
585
+ if (ext) {
586
+ if (await pathExists(candidateBase)) {
587
+ return candidateBase;
588
+ }
589
+ return null;
590
+ }
591
+ for (const extension of INTERNAL_EXTENSIONS) {
592
+ const withExtension = `${candidateBase}${extension}`;
593
+ if (await pathExists(withExtension)) {
594
+ return withExtension;
595
+ }
596
+ }
597
+ if (await pathExists(candidateBase)) {
598
+ const stats = await stat(candidateBase);
599
+ if (stats.isFile()) {
600
+ return candidateBase;
601
+ }
602
+ if (stats.isDirectory()) {
603
+ for (const extension of INTERNAL_EXTENSIONS) {
604
+ const indexCandidate = path2.join(candidateBase, `index${extension}`);
605
+ if (await pathExists(indexCandidate)) {
606
+ return indexCandidate;
607
+ }
608
+ }
609
+ }
610
+ }
611
+ return null;
612
+ }
613
+ async function resolveImportToFile(projectRoot, importerFilePath, specifier) {
614
+ const externalPackageName = getExternalPackageName(specifier);
615
+ if (externalPackageName) {
616
+ if (!BUILTIN_MODULES.has(externalPackageName)) {
617
+ return {
618
+ kind: "external",
619
+ packageName: externalPackageName
620
+ };
621
+ }
622
+ return { kind: "unknown" };
623
+ }
624
+ let candidateBase = null;
625
+ if (specifier.startsWith("@/")) {
626
+ candidateBase = path2.join(projectRoot, "src", specifier.slice(2));
627
+ } else if (specifier.startsWith("@root/")) {
628
+ candidateBase = path2.join(projectRoot, specifier.slice(6));
629
+ } else if (specifier.startsWith(".")) {
630
+ candidateBase = path2.resolve(path2.dirname(importerFilePath), specifier);
631
+ } else if (specifier.startsWith("/")) {
632
+ candidateBase = path2.join(projectRoot, specifier.slice(1));
633
+ } else {
634
+ return { kind: "unknown" };
635
+ }
636
+ const resolved = await resolveFileCandidate(candidateBase);
637
+ if (!resolved) {
638
+ return { kind: "unknown" };
639
+ }
640
+ return {
641
+ kind: "internal",
642
+ filePath: resolved
643
+ };
644
+ }
645
+ function shouldParseFile(filePath) {
646
+ return PARSEABLE_EXTENSIONS.has(path2.extname(filePath).toLowerCase());
647
+ }
648
+ async function buildImportGraph(input) {
649
+ const projectRoot = path2.resolve(input.projectRoot);
650
+ const queue = input.entryFiles.map((filePath) => path2.resolve(filePath));
651
+ const visited = /* @__PURE__ */ new Set();
652
+ const internalFiles = /* @__PURE__ */ new Set();
653
+ const externalPackages = /* @__PURE__ */ new Set();
654
+ while (queue.length > 0) {
655
+ const currentFilePath = queue.shift();
656
+ if (!currentFilePath || visited.has(currentFilePath)) {
657
+ continue;
658
+ }
659
+ visited.add(currentFilePath);
660
+ internalFiles.add(currentFilePath);
661
+ if (!shouldParseFile(currentFilePath)) {
662
+ continue;
663
+ }
664
+ let content = "";
665
+ try {
666
+ content = await readFile(currentFilePath, "utf-8");
667
+ } catch {
668
+ continue;
669
+ }
670
+ const specifiers = collectSpecifiers(content);
671
+ for (const specifier of specifiers) {
672
+ const resolved = await resolveImportToFile(
673
+ projectRoot,
674
+ currentFilePath,
675
+ specifier
676
+ );
677
+ if (resolved.kind === "internal") {
678
+ if (!visited.has(resolved.filePath)) {
679
+ queue.push(resolved.filePath);
680
+ }
681
+ continue;
682
+ }
683
+ if (resolved.kind === "external") {
684
+ externalPackages.add(resolved.packageName);
685
+ }
686
+ }
687
+ }
688
+ return {
689
+ internalFiles: [...internalFiles].sort(),
690
+ externalPackages: [...externalPackages].sort()
691
+ };
692
+ }
693
+
694
+ // src/lib/page-paths.ts
695
+ var WEBSITE_APP_ROOT = "src/app/(website)";
696
+ var PAGE_COMPONENTS_ROOT = "src/components/pages";
697
+ var normalizeSlug = (slug) => {
698
+ const trimmed = slug.trim();
699
+ if (!trimmed || trimmed === "/") {
700
+ return "/";
701
+ }
702
+ const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
703
+ const withoutTrailingSlash = withLeadingSlash.replace(/\/+$/, "");
704
+ return withoutTrailingSlash || "/";
705
+ };
706
+ var slugToFolderPath = (normalizedSlug) => {
707
+ if (normalizedSlug === "/") {
708
+ return "";
709
+ }
710
+ return normalizedSlug.slice(1);
711
+ };
712
+ var toPathSegment = (value) => {
713
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9/-]+/g, "-");
714
+ const compact = normalized.replace(/-+/g, "-").replace(/^-|-$/g, "");
715
+ return compact || "page";
716
+ };
717
+ var buildWebsitePagePath = (normalizedSlug) => {
718
+ if (normalizedSlug === "/") {
719
+ return `${WEBSITE_APP_ROOT}/page.tsx`;
720
+ }
721
+ return `${WEBSITE_APP_ROOT}${normalizedSlug}/page.tsx`;
722
+ };
723
+ var buildComponentsPath = (folderPath) => `${PAGE_COMPONENTS_ROOT}/${folderPath}`;
724
+ var resolveBlogSectionRoot = (slugFolderPath) => {
725
+ if (!slugFolderPath) {
726
+ return "blog";
727
+ }
728
+ const segments = slugFolderPath.split("/").filter(Boolean);
729
+ if (segments.length <= 1) {
730
+ return "blog";
731
+ }
732
+ return segments.slice(0, -1).join("/");
733
+ };
734
+ function resolvePrimeUiPageExportPaths({
735
+ pageType,
736
+ slug
737
+ }) {
738
+ const normalizedSlug = normalizeSlug(slug);
739
+ const slugFolderPath = slugToFolderPath(normalizedSlug);
740
+ switch (pageType) {
741
+ case "landing": {
742
+ const componentsFolder = slugFolderPath || "home";
743
+ return {
744
+ pagePath: buildWebsitePagePath(normalizedSlug),
745
+ componentsPath: buildComponentsPath(componentsFolder)
746
+ };
747
+ }
748
+ case "pricing":
749
+ case "contact-us": {
750
+ const componentsFolder = slugFolderPath || pageType;
751
+ return {
752
+ pagePath: buildWebsitePagePath(normalizedSlug),
753
+ componentsPath: buildComponentsPath(componentsFolder)
754
+ };
755
+ }
756
+ case "docs": {
757
+ const docsFolder = slugFolderPath || "docs";
758
+ return {
759
+ pagePath: `${WEBSITE_APP_ROOT}/${docsFolder}/[[...slug]]/page.tsx`,
760
+ componentsPath: buildComponentsPath("docs")
761
+ };
762
+ }
763
+ case "blogIndex": {
764
+ const blogFolder = slugFolderPath || "blog";
765
+ return {
766
+ pagePath: `${WEBSITE_APP_ROOT}/${blogFolder}/page.tsx`,
767
+ componentsPath: buildComponentsPath("blog")
768
+ };
769
+ }
770
+ case "blogPost": {
771
+ const blogSectionRoot = resolveBlogSectionRoot(slugFolderPath);
772
+ return {
773
+ pagePath: `${WEBSITE_APP_ROOT}/${blogSectionRoot}/[slug]/page.tsx`,
774
+ componentsPath: buildComponentsPath("blog")
775
+ };
776
+ }
777
+ case "legal":
778
+ return {
779
+ pagePath: buildWebsitePagePath(normalizedSlug),
780
+ componentsPath: buildComponentsPath("legal")
781
+ };
782
+ default: {
783
+ const fallbackFolder = slugFolderPath || toPathSegment(pageType);
784
+ return {
785
+ pagePath: buildWebsitePagePath(normalizedSlug),
786
+ componentsPath: buildComponentsPath(fallbackFolder)
787
+ };
788
+ }
789
+ }
790
+ }
791
+
792
+ // src/lib/text-diff.ts
793
+ var DEFAULT_MAX_DIFF_CHARS = 16e3;
794
+ var MAX_LCS_CELLS = 2e6;
795
+ function computeOps(oldLines, newLines) {
796
+ const oldLength = oldLines.length;
797
+ const newLength = newLines.length;
798
+ const table = Array.from(
799
+ { length: oldLength + 1 },
800
+ () => Array(newLength + 1).fill(0)
801
+ );
802
+ for (let i2 = oldLength - 1; i2 >= 0; i2 -= 1) {
803
+ for (let j2 = newLength - 1; j2 >= 0; j2 -= 1) {
804
+ if (oldLines[i2] === newLines[j2]) {
805
+ table[i2][j2] = table[i2 + 1][j2 + 1] + 1;
806
+ } else {
807
+ table[i2][j2] = Math.max(table[i2 + 1][j2], table[i2][j2 + 1]);
808
+ }
809
+ }
810
+ }
811
+ const ops = [];
812
+ let i = 0;
813
+ let j = 0;
814
+ while (i < oldLength && j < newLength) {
815
+ if (oldLines[i] === newLines[j]) {
816
+ ops.push({ kind: "equal", value: oldLines[i] ?? "" });
817
+ i += 1;
818
+ j += 1;
819
+ continue;
820
+ }
821
+ if (table[i + 1][j] >= table[i][j + 1]) {
822
+ ops.push({ kind: "delete", value: oldLines[i] ?? "" });
823
+ i += 1;
824
+ continue;
825
+ }
826
+ ops.push({ kind: "insert", value: newLines[j] ?? "" });
827
+ j += 1;
828
+ }
829
+ while (i < oldLength) {
830
+ ops.push({ kind: "delete", value: oldLines[i] ?? "" });
831
+ i += 1;
832
+ }
833
+ while (j < newLength) {
834
+ ops.push({ kind: "insert", value: newLines[j] ?? "" });
835
+ j += 1;
836
+ }
837
+ return ops;
838
+ }
839
+ function buildUnifiedDiff(input) {
840
+ const maxChars = input.maxChars ?? DEFAULT_MAX_DIFF_CHARS;
841
+ if (input.oldText === input.newText) {
842
+ return "";
843
+ }
844
+ const oldLines = input.oldText.split(/\r?\n/);
845
+ const newLines = input.newText.split(/\r?\n/);
846
+ const cells = (oldLines.length + 1) * (newLines.length + 1);
847
+ if (cells > MAX_LCS_CELLS) {
848
+ return [
849
+ `--- ${input.oldLabel}`,
850
+ `+++ ${input.newLabel}`,
851
+ "@@ diff omitted @@",
852
+ "Diff is too large to render safely."
853
+ ].join("\n");
854
+ }
855
+ const ops = computeOps(oldLines, newLines);
856
+ const output = [
857
+ `--- ${input.oldLabel}`,
858
+ `+++ ${input.newLabel}`,
859
+ `@@ -1,${oldLines.length} +1,${newLines.length} @@`
860
+ ];
861
+ for (const op of ops) {
862
+ if (op.kind === "equal") {
863
+ output.push(` ${op.value}`);
864
+ continue;
865
+ }
866
+ if (op.kind === "delete") {
867
+ output.push(`-${op.value}`);
868
+ continue;
869
+ }
870
+ output.push(`+${op.value}`);
871
+ }
872
+ const diff = output.join("\n");
873
+ if (diff.length <= maxChars) {
874
+ return diff;
875
+ }
876
+ return `${diff.slice(0, maxChars)}
877
+ ... [diff truncated]`;
878
+ }
879
+
880
+ // src/services/page-copy-service.ts
881
+ var DEPENDENCY_SECTIONS = [
882
+ "dependencies",
883
+ "devDependencies",
884
+ "peerDependencies"
885
+ ];
886
+ var REWRITABLE_IMPORT_EXTENSIONS = /* @__PURE__ */ new Set([
887
+ ".ts",
888
+ ".tsx",
889
+ ".js",
890
+ ".jsx",
891
+ ".mjs",
892
+ ".cjs"
893
+ ]);
894
+ function normalizeSlug2(slug) {
895
+ const trimmed = slug.trim();
896
+ if (!trimmed || trimmed === "/") {
897
+ return "/";
898
+ }
899
+ const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
900
+ return withLeadingSlash.replace(/\/+$/, "") || "/";
901
+ }
902
+ function toPosixPath(value) {
903
+ return value.split(path3.sep).join("/");
904
+ }
905
+ function toProjectRelative(rootPath, absolutePath) {
906
+ return toPosixPath(path3.relative(rootPath, absolutePath));
907
+ }
908
+ function escapeRegExp(value) {
909
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
910
+ }
911
+ function isBinaryBuffer(buffer) {
912
+ const limit = Math.min(buffer.length, 8e3);
913
+ for (let index = 0; index < limit; index += 1) {
914
+ if (buffer[index] === 0) {
915
+ return true;
916
+ }
917
+ }
918
+ return false;
919
+ }
920
+ function stripSrcPrefix(relativePath) {
921
+ return relativePath.replace(/^src\//, "");
922
+ }
923
+ function buildImportRewritePlan(sourceComponentsPath, targetComponentsPath) {
924
+ const normalizedSource = toPosixPath(sourceComponentsPath);
925
+ const normalizedTarget = toPosixPath(targetComponentsPath);
926
+ if (normalizedSource === normalizedTarget) {
927
+ return null;
928
+ }
929
+ const sourceAtAliasPrefix = `@/${stripSrcPrefix(normalizedSource)}`;
930
+ const targetAtAliasPrefix = `@/${stripSrcPrefix(normalizedTarget)}`;
931
+ const sourceRootAliasPrefix = `@root/${normalizedSource}`;
932
+ const targetRootAliasPrefix = `@root/${normalizedTarget}`;
933
+ return {
934
+ sourceAtAliasPrefix,
935
+ targetAtAliasPrefix,
936
+ sourceRootAliasPrefix,
937
+ targetRootAliasPrefix
938
+ };
939
+ }
940
+ function replaceAliasPrefixInImportStrings(content, sourcePrefix, targetPrefix) {
941
+ const pattern = new RegExp(
942
+ `(["'\`])${escapeRegExp(sourcePrefix)}(?=\\/|\\1)`,
943
+ "g"
944
+ );
945
+ return content.replace(pattern, `$1${targetPrefix}`);
946
+ }
947
+ function rewriteImportsForRemappedSlug(content, plan) {
948
+ const afterAtAlias = replaceAliasPrefixInImportStrings(
949
+ content,
950
+ plan.sourceAtAliasPrefix,
951
+ plan.targetAtAliasPrefix
952
+ );
953
+ return replaceAliasPrefixInImportStrings(
954
+ afterAtAlias,
955
+ plan.sourceRootAliasPrefix,
956
+ plan.targetRootAliasPrefix
957
+ );
958
+ }
959
+ function buildPlannedSourceBuffer(sourceBuffer, sourceFilePath, importRewritePlan) {
960
+ if (!importRewritePlan) {
961
+ return sourceBuffer;
962
+ }
963
+ const extension = path3.extname(sourceFilePath).toLowerCase();
964
+ if (!REWRITABLE_IMPORT_EXTENSIONS.has(extension)) {
965
+ return sourceBuffer;
966
+ }
967
+ if (isBinaryBuffer(sourceBuffer)) {
968
+ return sourceBuffer;
969
+ }
970
+ const sourceText = sourceBuffer.toString("utf-8");
971
+ const rewritten = rewriteImportsForRemappedSlug(sourceText, importRewritePlan);
972
+ if (rewritten === sourceText) {
973
+ return sourceBuffer;
974
+ }
975
+ return Buffer.from(rewritten, "utf-8");
976
+ }
977
+ async function readJsonFile(filePath) {
978
+ const content = await readFile2(filePath, "utf-8");
979
+ return JSON.parse(content);
980
+ }
981
+ async function fileExists(filePath) {
982
+ try {
983
+ await stat2(filePath);
984
+ return true;
985
+ } catch {
986
+ return false;
987
+ }
988
+ }
989
+ function isPageRecord(value) {
990
+ if (!value || typeof value !== "object") {
991
+ return false;
992
+ }
993
+ const maybe = value;
994
+ return typeof maybe.id === "string" && typeof maybe.title === "string" && typeof maybe.slug === "string" && typeof maybe.pageType === "string" && typeof maybe.isReadyToExport === "boolean" && typeof maybe.pagePath === "string" && typeof maybe.componentsPath === "string";
995
+ }
996
+ function parseManifest(value) {
997
+ if (!value || typeof value !== "object") {
998
+ throw new Error("Invalid export manifest format.");
999
+ }
1000
+ const maybe = value;
1001
+ if (maybe.schemaVersion !== 1 || typeof maybe.generatedAt !== "string" || typeof maybe.exportId !== "string" || typeof maybe.projectPath !== "string" || !Array.isArray(maybe.pages)) {
1002
+ throw new Error("Export manifest does not match expected schema.");
1003
+ }
1004
+ if (!maybe.pages.every(isPageRecord)) {
1005
+ throw new Error("Export manifest pages payload is invalid.");
1006
+ }
1007
+ return {
1008
+ schemaVersion: 1,
1009
+ generatedAt: maybe.generatedAt,
1010
+ exportId: maybe.exportId,
1011
+ projectPath: maybe.projectPath,
1012
+ pages: maybe.pages
1013
+ };
1014
+ }
1015
+ async function listFilesRecursively(dirPath) {
1016
+ const files = [];
1017
+ const stack = [dirPath];
1018
+ while (stack.length > 0) {
1019
+ const current = stack.pop();
1020
+ if (!current) {
1021
+ continue;
1022
+ }
1023
+ const entries = await readdir(current, { withFileTypes: true });
1024
+ for (const entry of entries) {
1025
+ const absolutePath = path3.join(current, entry.name);
1026
+ if (entry.isDirectory()) {
1027
+ stack.push(absolutePath);
1028
+ continue;
1029
+ }
1030
+ if (entry.isFile()) {
1031
+ files.push(absolutePath);
1032
+ }
1033
+ }
1034
+ }
1035
+ return files;
1036
+ }
1037
+ async function resolveSingleExportDirectory(exportsRoot) {
1038
+ const entries = await readdir(exportsRoot, { withFileTypes: true });
1039
+ const exportDirectories = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
1040
+ if (exportDirectories.length === 0) {
1041
+ throw new Error(
1042
+ "No downloaded exports found in .primeui/temp/exports. Run primeui_download_export first."
1043
+ );
1044
+ }
1045
+ if (exportDirectories.length > 1) {
1046
+ throw new Error(
1047
+ "Multiple export folders found in .primeui/temp/exports. Clear temp with primeui_clear_temp and download one export."
1048
+ );
1049
+ }
1050
+ const exportId = exportDirectories[0] ?? "";
1051
+ return {
1052
+ exportId,
1053
+ exportPath: path3.join(exportsRoot, exportId)
1054
+ };
1055
+ }
1056
+ function ensureSafeTargetPath(projectRoot, targetPath) {
1057
+ const relative = path3.relative(projectRoot, targetPath);
1058
+ if (relative.startsWith("..") || path3.isAbsolute(relative)) {
1059
+ throw new Error(
1060
+ `Refusing to write outside project root. Computed target: ${targetPath}`
1061
+ );
1062
+ }
1063
+ }
1064
+ function getDependencyVersion(packageJson, packageName) {
1065
+ for (const section of DEPENDENCY_SECTIONS) {
1066
+ const sectionData = packageJson[section];
1067
+ if (!sectionData || typeof sectionData !== "object") {
1068
+ continue;
1069
+ }
1070
+ const value = sectionData[packageName];
1071
+ if (typeof value === "string") {
1072
+ return {
1073
+ section,
1074
+ version: value
1075
+ };
1076
+ }
1077
+ }
1078
+ return null;
1079
+ }
1080
+ function getOrCreateDependencySection(packageJson, section) {
1081
+ const existing = packageJson[section];
1082
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
1083
+ const created = {};
1084
+ packageJson[section] = created;
1085
+ return created;
1086
+ }
1087
+ return existing;
1088
+ }
1089
+ function sortDependencySections(packageJson) {
1090
+ for (const section of DEPENDENCY_SECTIONS) {
1091
+ const sectionData = packageJson[section];
1092
+ if (!sectionData || typeof sectionData !== "object" || Array.isArray(sectionData)) {
1093
+ continue;
1094
+ }
1095
+ const sorted = Object.fromEntries(
1096
+ Object.entries(sectionData).sort(
1097
+ ([a], [b]) => a.localeCompare(b)
1098
+ )
1099
+ );
1100
+ packageJson[section] = sorted;
1101
+ }
1102
+ }
1103
+ function resolveTargetFilePath(input) {
1104
+ const {
1105
+ sourceFilePath,
1106
+ sourcePagePath,
1107
+ sourceComponentsPath,
1108
+ targetPagePath,
1109
+ targetComponentsPath,
1110
+ exportRoot,
1111
+ projectRoot
1112
+ } = input;
1113
+ if (sourceFilePath === sourcePagePath) {
1114
+ return targetPagePath;
1115
+ }
1116
+ const componentsPrefix = `${sourceComponentsPath}${path3.sep}`;
1117
+ if (sourceFilePath === sourceComponentsPath || sourceFilePath.startsWith(componentsPrefix)) {
1118
+ const relativeToComponents = path3.relative(
1119
+ sourceComponentsPath,
1120
+ sourceFilePath
1121
+ );
1122
+ return path3.join(targetComponentsPath, relativeToComponents);
1123
+ }
1124
+ const relativeToExport = path3.relative(exportRoot, sourceFilePath);
1125
+ if (relativeToExport.startsWith("..") || path3.isAbsolute(relativeToExport)) {
1126
+ throw new Error(`Source file is outside export root: ${sourceFilePath}`);
1127
+ }
1128
+ return path3.join(projectRoot, relativeToExport);
1129
+ }
1130
+ async function copyPageFromExport(input) {
1131
+ const normalizedOriginSlug = normalizeSlug2(input.originPageSlug);
1132
+ const normalizedActualSlug = normalizeSlug2(
1133
+ input.actualPageSlug ?? input.originPageSlug
1134
+ );
1135
+ const isSlugRemapped = normalizedActualSlug !== normalizedOriginSlug;
1136
+ const { exportId, exportPath } = await resolveSingleExportDirectory(
1137
+ input.exportsRoot
1138
+ );
1139
+ const manifestPath = path3.join(
1140
+ input.exportsRoot,
1141
+ `${exportId}.manifest.json`
1142
+ );
1143
+ if (!await fileExists(manifestPath)) {
1144
+ throw new Error(
1145
+ `Export manifest is missing at ${manifestPath}. Run primeui_download_export to regenerate it.`
1146
+ );
1147
+ }
1148
+ const manifest = parseManifest(await readJsonFile(manifestPath));
1149
+ if (manifest.exportId !== exportId) {
1150
+ throw new Error(
1151
+ `Manifest exportId mismatch: expected ${exportId}, got ${manifest.exportId}. Re-download export.`
1152
+ );
1153
+ }
1154
+ const page = manifest.pages.find(
1155
+ (item) => normalizeSlug2(item.slug) === normalizedOriginSlug
1156
+ );
1157
+ if (!page) {
1158
+ throw new Error(
1159
+ `Page not found in manifest for slug: ${normalizedOriginSlug}`
1160
+ );
1161
+ }
1162
+ const sourcePagePath = path3.join(exportPath, page.pagePath);
1163
+ const sourceComponentsPath = path3.join(exportPath, page.componentsPath);
1164
+ const sourcePageStats = await stat2(sourcePagePath).catch(() => null);
1165
+ if (!sourcePageStats?.isFile()) {
1166
+ throw new Error(`Source page file not found: ${sourcePagePath}`);
1167
+ }
1168
+ const sourceComponentsStats = await stat2(sourceComponentsPath).catch(
1169
+ () => null
1170
+ );
1171
+ if (!sourceComponentsStats?.isDirectory()) {
1172
+ throw new Error(
1173
+ `Source components folder not found: ${sourceComponentsPath}`
1174
+ );
1175
+ }
1176
+ const targetPaths = normalizedActualSlug === normalizedOriginSlug ? {
1177
+ pagePath: page.pagePath,
1178
+ componentsPath: page.componentsPath
1179
+ } : resolvePrimeUiPageExportPaths({
1180
+ pageType: page.pageType,
1181
+ slug: normalizedActualSlug
1182
+ });
1183
+ const targetPagePath = path3.join(input.projectRoot, targetPaths.pagePath);
1184
+ const targetComponentsPath = path3.join(
1185
+ input.projectRoot,
1186
+ targetPaths.componentsPath
1187
+ );
1188
+ const importRewritePlan = isSlugRemapped ? buildImportRewritePlan(page.componentsPath, targetPaths.componentsPath) : null;
1189
+ ensureSafeTargetPath(input.projectRoot, targetPagePath);
1190
+ ensureSafeTargetPath(input.projectRoot, targetComponentsPath);
1191
+ const componentFiles = await listFilesRecursively(sourceComponentsPath);
1192
+ const seedFiles = [sourcePagePath, ...componentFiles];
1193
+ const importGraph = await buildImportGraph({
1194
+ projectRoot: exportPath,
1195
+ entryFiles: seedFiles
1196
+ });
1197
+ const candidateFiles = [...new Set(importGraph.internalFiles)].sort(
1198
+ (a, b) => a.localeCompare(b)
1199
+ );
1200
+ const newFiles = [];
1201
+ const identicalFiles = [];
1202
+ const conflictFiles = [];
1203
+ for (const sourceFilePath of candidateFiles) {
1204
+ const targetFilePath = resolveTargetFilePath({
1205
+ sourceFilePath,
1206
+ sourcePagePath,
1207
+ sourceComponentsPath,
1208
+ targetPagePath,
1209
+ targetComponentsPath,
1210
+ exportRoot: exportPath,
1211
+ projectRoot: input.projectRoot
1212
+ });
1213
+ ensureSafeTargetPath(input.projectRoot, targetFilePath);
1214
+ const sourceBuffer = await readFile2(sourceFilePath);
1215
+ const plannedSourceBuffer = buildPlannedSourceBuffer(
1216
+ sourceBuffer,
1217
+ sourceFilePath,
1218
+ importRewritePlan
1219
+ );
1220
+ let targetBuffer = null;
1221
+ try {
1222
+ targetBuffer = await readFile2(targetFilePath);
1223
+ } catch {
1224
+ targetBuffer = null;
1225
+ }
1226
+ const sourceRelative = toProjectRelative(exportPath, sourceFilePath);
1227
+ const targetRelative = toProjectRelative(input.projectRoot, targetFilePath);
1228
+ if (!targetBuffer) {
1229
+ await ensureDir(path3.dirname(targetFilePath));
1230
+ await writeFile2(targetFilePath, plannedSourceBuffer);
1231
+ newFiles.push({
1232
+ sourcePath: sourceRelative,
1233
+ targetPath: targetRelative
1234
+ });
1235
+ continue;
1236
+ }
1237
+ if (plannedSourceBuffer.equals(targetBuffer)) {
1238
+ identicalFiles.push({
1239
+ sourcePath: sourceRelative,
1240
+ targetPath: targetRelative
1241
+ });
1242
+ continue;
1243
+ }
1244
+ const isBinary = isBinaryBuffer(plannedSourceBuffer) || isBinaryBuffer(targetBuffer);
1245
+ const diff = isBinary ? "Binary files differ." : buildUnifiedDiff({
1246
+ oldText: targetBuffer.toString("utf-8"),
1247
+ newText: plannedSourceBuffer.toString("utf-8"),
1248
+ oldLabel: `user/${targetRelative}`,
1249
+ newLabel: `export/${sourceRelative}`
1250
+ });
1251
+ conflictFiles.push({
1252
+ sourcePath: sourceRelative,
1253
+ targetPath: targetRelative,
1254
+ diff,
1255
+ isBinary
1256
+ });
1257
+ }
1258
+ const exportPackageJsonPath = path3.join(exportPath, "package.json");
1259
+ const userPackageJsonPath = path3.join(input.projectRoot, "package.json");
1260
+ if (!await fileExists(exportPackageJsonPath)) {
1261
+ throw new Error(`Export package.json not found: ${exportPackageJsonPath}`);
1262
+ }
1263
+ if (!await fileExists(userPackageJsonPath)) {
1264
+ throw new Error(`User package.json not found: ${userPackageJsonPath}`);
1265
+ }
1266
+ const exportPackageJson = await readJsonFile(
1267
+ exportPackageJsonPath
1268
+ );
1269
+ const userPackageJson = await readJsonFile(userPackageJsonPath);
1270
+ const addedDependencies = [];
1271
+ const dependenciesVersionConflicts = [];
1272
+ for (const packageName of importGraph.externalPackages) {
1273
+ const exportDependency = getDependencyVersion(
1274
+ exportPackageJson,
1275
+ packageName
1276
+ );
1277
+ if (!exportDependency) {
1278
+ continue;
1279
+ }
1280
+ const userDependency = getDependencyVersion(userPackageJson, packageName);
1281
+ if (!userDependency) {
1282
+ const sectionData = getOrCreateDependencySection(
1283
+ userPackageJson,
1284
+ exportDependency.section
1285
+ );
1286
+ sectionData[packageName] = exportDependency.version;
1287
+ addedDependencies.push({
1288
+ packageName,
1289
+ version: exportDependency.version,
1290
+ section: exportDependency.section
1291
+ });
1292
+ continue;
1293
+ }
1294
+ if (userDependency.version !== exportDependency.version) {
1295
+ dependenciesVersionConflicts.push({
1296
+ packageName,
1297
+ section: exportDependency.section,
1298
+ exportVersion: exportDependency.version,
1299
+ userVersion: userDependency.version
1300
+ });
1301
+ }
1302
+ }
1303
+ if (addedDependencies.length > 0) {
1304
+ sortDependencySections(userPackageJson);
1305
+ await writeFile2(
1306
+ userPackageJsonPath,
1307
+ `${JSON.stringify(userPackageJson, null, 2)}
1308
+ `,
1309
+ "utf-8"
1310
+ );
1311
+ }
1312
+ return {
1313
+ exportId,
1314
+ exportPath,
1315
+ originPageSlug: normalizedOriginSlug,
1316
+ actualPageSlug: normalizedActualSlug,
1317
+ sourcePagePath: page.pagePath,
1318
+ targetPagePath: targetPaths.pagePath,
1319
+ sourceComponentsPath: page.componentsPath,
1320
+ targetComponentsPath: targetPaths.componentsPath,
1321
+ newFiles,
1322
+ identicalFiles,
1323
+ conflictFiles,
1324
+ addedDependencies: addedDependencies.sort(
1325
+ (a, b) => a.packageName.localeCompare(b.packageName)
1326
+ ),
1327
+ dependenciesVersionConflicts: dependenciesVersionConflicts.sort(
1328
+ (a, b) => a.packageName.localeCompare(b.packageName)
1329
+ ),
1330
+ summary: {
1331
+ totalCandidateFiles: candidateFiles.length,
1332
+ copiedFiles: newFiles.length,
1333
+ identicalFiles: identicalFiles.length,
1334
+ conflictFiles: conflictFiles.length,
1335
+ addedDependencies: addedDependencies.length,
1336
+ dependenciesVersionConflicts: dependenciesVersionConflicts.length
1337
+ }
1338
+ };
1339
+ }
1340
+
392
1341
  // src/services/project-sync-service.ts
1342
+ function isProjectPage(value) {
1343
+ if (!value || typeof value !== "object") {
1344
+ return false;
1345
+ }
1346
+ const maybe = value;
1347
+ return typeof maybe.id === "string" && typeof maybe.title === "string" && typeof maybe.slug === "string" && typeof maybe.pageType === "string" && typeof maybe.isReadyToExport === "boolean" && typeof maybe.pagePath === "string" && typeof maybe.componentsPath === "string";
1348
+ }
1349
+ function buildPagesSnapshotPath(exportsRoot, exportId) {
1350
+ return path4.join(exportsRoot, `${exportId}.pages.snapshot.json`);
1351
+ }
1352
+ function parsePagesSnapshot(value) {
1353
+ if (!value || typeof value !== "object") {
1354
+ throw new Error("Export pages snapshot is invalid.");
1355
+ }
1356
+ const maybe = value;
1357
+ if (maybe.schemaVersion !== 1 || typeof maybe.generatedAt !== "string" || typeof maybe.exportId !== "string" || !Array.isArray(maybe.pages)) {
1358
+ throw new Error("Export pages snapshot does not match expected schema.");
1359
+ }
1360
+ if (!maybe.pages.every(isProjectPage)) {
1361
+ throw new Error("Export pages snapshot has invalid pages payload.");
1362
+ }
1363
+ return {
1364
+ schemaVersion: 1,
1365
+ generatedAt: maybe.generatedAt,
1366
+ exportId: maybe.exportId,
1367
+ pages: maybe.pages
1368
+ };
1369
+ }
393
1370
  var ProjectSyncService = class {
394
1371
  provider;
395
1372
  projectRoot;
@@ -399,9 +1376,9 @@ var ProjectSyncService = class {
399
1376
  constructor(options) {
400
1377
  this.projectRoot = options.projectRoot;
401
1378
  this.provider = options.provider;
402
- this.primeUiRoot = path2.join(this.projectRoot, ".primeui");
403
- this.tempRoot = path2.join(this.primeUiRoot, "temp");
404
- this.exportsRoot = path2.join(this.tempRoot, "exports");
1379
+ this.primeUiRoot = path4.join(this.projectRoot, ".primeui");
1380
+ this.tempRoot = path4.join(this.primeUiRoot, "temp");
1381
+ this.exportsRoot = path4.join(this.tempRoot, "exports");
405
1382
  }
406
1383
  async getProjectInfo() {
407
1384
  await this.ensureTempLayout();
@@ -413,7 +1390,23 @@ var ProjectSyncService = class {
413
1390
  }
414
1391
  async createExport() {
415
1392
  await this.ensureTempLayout();
416
- return this.provider.createExport();
1393
+ const result = await this.provider.createExport();
1394
+ const pagesSnapshotPath = buildPagesSnapshotPath(
1395
+ this.exportsRoot,
1396
+ result.export.id
1397
+ );
1398
+ const snapshot = {
1399
+ schemaVersion: 1,
1400
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1401
+ exportId: result.export.id,
1402
+ pages: result.pages
1403
+ };
1404
+ await writeUtf8(
1405
+ pagesSnapshotPath,
1406
+ `${JSON.stringify(snapshot, null, 2)}
1407
+ `
1408
+ );
1409
+ return result;
417
1410
  }
418
1411
  async downloadExportById(id) {
419
1412
  await this.ensureTempLayout();
@@ -422,36 +1415,74 @@ var ProjectSyncService = class {
422
1415
  if (!selected) {
423
1416
  throw new Error(`Export not found: ${id}`);
424
1417
  }
425
- const targetZipPath = path2.join(this.exportsRoot, `${id}.zip`);
426
- const targetProjectPath = path2.join(this.exportsRoot, id);
1418
+ const targetZipPath = path4.join(this.exportsRoot, `${id}.zip`);
1419
+ const targetProjectPath = path4.join(this.exportsRoot, id);
427
1420
  await ensureDir(this.exportsRoot);
428
1421
  await this.provider.downloadExportArchive(id, targetZipPath);
429
1422
  await resetDir(targetProjectPath);
430
1423
  await extractZip(targetZipPath, targetProjectPath);
431
- const { pages } = await this.provider.getProjectInfo();
1424
+ const pagesSnapshotPath = buildPagesSnapshotPath(this.exportsRoot, id);
1425
+ let pagesSnapshot;
1426
+ try {
1427
+ const snapshotRaw = JSON.parse(
1428
+ await readFile3(pagesSnapshotPath, "utf-8")
1429
+ );
1430
+ pagesSnapshot = parsePagesSnapshot(snapshotRaw);
1431
+ } catch (error) {
1432
+ const reason = error instanceof Error ? error.message : String(error);
1433
+ throw new Error(
1434
+ `Export pages snapshot is required for download id "${id}". Run primeui_create_export for this export before downloading. Details: ${reason}`
1435
+ );
1436
+ }
1437
+ if (pagesSnapshot.exportId !== id) {
1438
+ throw new Error(
1439
+ `Export pages snapshot mismatch for id "${id}". Run primeui_create_export and retry download.`
1440
+ );
1441
+ }
1442
+ const pages = pagesSnapshot.pages;
1443
+ const manifestPath = path4.join(this.exportsRoot, `${id}.manifest.json`);
1444
+ const manifest = {
1445
+ schemaVersion: 1,
1446
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1447
+ exportId: id,
1448
+ projectPath: targetProjectPath,
1449
+ pages
1450
+ };
1451
+ await writeUtf8(manifestPath, `${JSON.stringify(manifest, null, 2)}
1452
+ `);
432
1453
  return {
433
1454
  exportId: id,
434
1455
  projectPath: targetProjectPath,
1456
+ manifestPath,
435
1457
  pages
436
1458
  };
437
1459
  }
1460
+ async copyPage(originPageSlug, actualPageSlug) {
1461
+ await this.ensureTempLayout();
1462
+ return copyPageFromExport({
1463
+ projectRoot: this.projectRoot,
1464
+ exportsRoot: this.exportsRoot,
1465
+ originPageSlug,
1466
+ actualPageSlug
1467
+ });
1468
+ }
438
1469
  async clearTemp() {
439
1470
  await resetDir(this.tempRoot);
440
- await writeUtf8(path2.join(this.tempRoot, ".gitkeep"), "");
1471
+ await writeUtf8(path4.join(this.tempRoot, ".gitkeep"), "");
441
1472
  await ensureDir(this.exportsRoot);
442
1473
  }
443
1474
  async ensureTempLayout() {
444
1475
  await ensureDir(this.primeUiRoot);
445
1476
  await ensureDir(this.tempRoot);
446
1477
  await ensureDir(this.exportsRoot);
447
- await writeUtf8(path2.join(this.tempRoot, ".gitkeep"), "");
1478
+ await writeUtf8(path4.join(this.tempRoot, ".gitkeep"), "");
448
1479
  }
449
1480
  };
450
1481
 
451
1482
  // src/sources/api-provider.ts
452
1483
  import { createWriteStream } from "fs";
453
1484
  import { unlink } from "fs/promises";
454
- import path3 from "path";
1485
+ import path5 from "path";
455
1486
  import { Readable, Transform } from "stream";
456
1487
  import { pipeline } from "stream/promises";
457
1488
  import { z as z2 } from "zod";
@@ -620,7 +1651,7 @@ var ApiProjectDataProvider = class {
620
1651
  if (!response.body) {
621
1652
  throw new PrimeUiApiContractError(endpoint, "response body is empty");
622
1653
  }
623
- await ensureDir(path3.dirname(destinationPath));
1654
+ await ensureDir(path5.dirname(destinationPath));
624
1655
  const zipStream = Readable.fromWeb(
625
1656
  response.body
626
1657
  );