@primeuicom/mcp 0.1.10 → 0.1.12
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/README.md +23 -11
- package/dist/service.js +1247 -104
- package/dist/service.js.map +1 -1
- package/package.json +1 -1
package/dist/service.js
CHANGED
|
@@ -1,48 +1,179 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/service.ts
|
|
4
|
+
import path7 from "path";
|
|
4
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
6
|
|
|
7
|
+
// src/lib/project-link-config.ts
|
|
8
|
+
import { readFile, stat } from "fs/promises";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
var PRIMEUI_PROJECT_CONFIG_RELATIVE_PATH = ".primeui/project.json";
|
|
12
|
+
var primeUiProjectConfigSchema = z.object({
|
|
13
|
+
projectId: z.string().trim().min(1),
|
|
14
|
+
apiKey: z.string().trim().min(1),
|
|
15
|
+
targetProjectPath: z.string().trim().regex(/^\.\/.*$/)
|
|
16
|
+
}).passthrough();
|
|
17
|
+
async function fileExists(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
const fileStats = await stat(filePath);
|
|
20
|
+
return fileStats.isFile();
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function ancestors(startPath) {
|
|
26
|
+
const resolvedStartPath = path.resolve(startPath);
|
|
27
|
+
const directories = [];
|
|
28
|
+
let currentPath = resolvedStartPath;
|
|
29
|
+
while (true) {
|
|
30
|
+
directories.push(currentPath);
|
|
31
|
+
const parentPath = path.dirname(currentPath);
|
|
32
|
+
if (parentPath === currentPath) {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
currentPath = parentPath;
|
|
36
|
+
}
|
|
37
|
+
return directories;
|
|
38
|
+
}
|
|
39
|
+
async function findPrimeUiProjectConfigPath(startPath) {
|
|
40
|
+
for (const currentPath of ancestors(startPath)) {
|
|
41
|
+
const candidatePath = path.join(
|
|
42
|
+
currentPath,
|
|
43
|
+
PRIMEUI_PROJECT_CONFIG_RELATIVE_PATH
|
|
44
|
+
);
|
|
45
|
+
if (await fileExists(candidatePath)) {
|
|
46
|
+
return candidatePath;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
async function readPrimeUiProjectConfig(projectConfigPath) {
|
|
52
|
+
let projectConfigRaw;
|
|
53
|
+
try {
|
|
54
|
+
projectConfigRaw = await readFile(projectConfigPath, "utf-8");
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`[primeui-mcp] failed to read ${PRIMEUI_PROJECT_CONFIG_RELATIVE_PATH}: ${error instanceof Error ? error.message : String(error)}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
let projectConfigJson;
|
|
61
|
+
try {
|
|
62
|
+
projectConfigJson = JSON.parse(projectConfigRaw);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`[primeui-mcp] invalid JSON in ${PRIMEUI_PROJECT_CONFIG_RELATIVE_PATH}: ${error instanceof Error ? error.message : String(error)}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const parsedConfig = primeUiProjectConfigSchema.safeParse(projectConfigJson);
|
|
69
|
+
if (!parsedConfig.success) {
|
|
70
|
+
const details = parsedConfig.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
|
|
71
|
+
throw new Error(
|
|
72
|
+
`[primeui-mcp] invalid ${PRIMEUI_PROJECT_CONFIG_RELATIVE_PATH} format: ${details}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return parsedConfig.data;
|
|
76
|
+
}
|
|
77
|
+
async function resolvePrimeUiProjectConfig(options) {
|
|
78
|
+
const envProjectRoot = options.projectRootFromEnv?.trim();
|
|
79
|
+
const projectConfigPath = envProjectRoot ? path.join(path.resolve(envProjectRoot), PRIMEUI_PROJECT_CONFIG_RELATIVE_PATH) : await findPrimeUiProjectConfigPath(options.cwd);
|
|
80
|
+
if (!projectConfigPath) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`[primeui-mcp] ${PRIMEUI_PROJECT_CONFIG_RELATIVE_PATH} not found from cwd (${options.cwd}).`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const projectRoot = path.dirname(path.dirname(projectConfigPath));
|
|
86
|
+
const projectConfig = await readPrimeUiProjectConfig(projectConfigPath);
|
|
87
|
+
return {
|
|
88
|
+
projectRoot,
|
|
89
|
+
projectConfigPath,
|
|
90
|
+
projectConfig
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function resolvePrimeUiApiKey(options) {
|
|
94
|
+
const envApiKey = options.apiKeyFromEnv?.trim();
|
|
95
|
+
if (envApiKey) {
|
|
96
|
+
return envApiKey;
|
|
97
|
+
}
|
|
98
|
+
const projectConfigApiKey = options.projectConfig.apiKey.trim();
|
|
99
|
+
if (!projectConfigApiKey) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"[primeui-mcp] PRIMEUI_API_KEY is missing in env and .primeui/project.json"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return projectConfigApiKey;
|
|
105
|
+
}
|
|
106
|
+
|
|
6
107
|
// src/server.ts
|
|
7
108
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
109
|
|
|
9
110
|
// src/instructions.ts
|
|
10
|
-
import { z } from "zod/v3";
|
|
11
|
-
var pageSchema =
|
|
12
|
-
id:
|
|
111
|
+
import { z as z2 } from "zod/v3";
|
|
112
|
+
var pageSchema = z2.object({
|
|
113
|
+
id: z2.string().describe(
|
|
13
114
|
"Stable PrimeUI page identifier. Use this to match the same page across tool responses."
|
|
14
115
|
),
|
|
15
|
-
title:
|
|
116
|
+
title: z2.string().describe(
|
|
16
117
|
"Human-readable page title. Show this to the user when confirming which pages to import."
|
|
17
118
|
),
|
|
18
|
-
slug:
|
|
119
|
+
slug: z2.string().describe(
|
|
19
120
|
"PrimeUI route slug, for example '/', '/pricing', '/docs/getting-started'. Use to match user intent."
|
|
20
121
|
),
|
|
21
|
-
pageType:
|
|
122
|
+
pageType: z2.string().describe(
|
|
22
123
|
"PrimeUI page type classification (for example landing, docs, pricing, blogIndex, legal)."
|
|
23
124
|
),
|
|
24
|
-
isReadyToExport:
|
|
125
|
+
isReadyToExport: z2.boolean().describe(
|
|
25
126
|
"True when this page has an active export-ready variant. Only ready pages are expected in export output."
|
|
26
127
|
),
|
|
27
|
-
pagePath:
|
|
128
|
+
pagePath: z2.string().describe(
|
|
28
129
|
"Relative file path to the page source inside downloaded export root. Copy page code from this exact path."
|
|
29
130
|
),
|
|
30
|
-
componentsPath:
|
|
131
|
+
componentsPath: z2.string().describe(
|
|
31
132
|
"Relative directory path inside downloaded export root where page-level components live. Use as dependency copy start."
|
|
32
133
|
)
|
|
33
134
|
}).describe(
|
|
34
135
|
"PrimeUI page descriptor used by project and export tools. Paths are always relative to export project root."
|
|
35
136
|
);
|
|
36
|
-
var exportStatusSchema =
|
|
137
|
+
var exportStatusSchema = z2.enum(["in_progress", "completed", "failed"]).describe(
|
|
37
138
|
"Export lifecycle state. Use 'completed' before calling primeui_download_export."
|
|
38
139
|
);
|
|
39
|
-
var exportItemSchema =
|
|
40
|
-
id:
|
|
140
|
+
var exportItemSchema = z2.object({
|
|
141
|
+
id: z2.string().describe(
|
|
41
142
|
"Export identifier. Pass this value to primeui_download_export input.id."
|
|
42
143
|
),
|
|
43
144
|
status: exportStatusSchema,
|
|
44
|
-
createdAt:
|
|
145
|
+
createdAt: z2.string().describe("Export creation timestamp in ISO-8601 UTC format.")
|
|
45
146
|
}).describe("Single export record from PrimeUI export history.");
|
|
147
|
+
var fileTransferSchema = z2.object({
|
|
148
|
+
sourcePath: z2.string().describe("Relative source file path inside downloaded export root."),
|
|
149
|
+
targetPath: z2.string().describe("Relative target file path inside user project root.")
|
|
150
|
+
});
|
|
151
|
+
var conflictFileSchema = fileTransferSchema.extend({
|
|
152
|
+
diff: z2.string().describe(
|
|
153
|
+
"Unified diff between user file and export file. For binary files the diff contains a plain message."
|
|
154
|
+
),
|
|
155
|
+
isBinary: z2.boolean().describe("True if conflict comparison was treated as binary data.")
|
|
156
|
+
});
|
|
157
|
+
var dependencySectionSchema = z2.enum([
|
|
158
|
+
"dependencies",
|
|
159
|
+
"devDependencies",
|
|
160
|
+
"peerDependencies"
|
|
161
|
+
]);
|
|
162
|
+
var dependencyToAddSchema = z2.object({
|
|
163
|
+
packageName: z2.string().describe("Dependency package name that was missing in user package.json."),
|
|
164
|
+
version: z2.string().describe("Version from exported project's package.json."),
|
|
165
|
+
section: dependencySectionSchema.describe(
|
|
166
|
+
"package.json section where dependency should be added."
|
|
167
|
+
)
|
|
168
|
+
});
|
|
169
|
+
var dependencyVersionConflictSchema = z2.object({
|
|
170
|
+
packageName: z2.string().describe("Dependency package name with version mismatch."),
|
|
171
|
+
section: dependencySectionSchema.describe(
|
|
172
|
+
"Section where export expects this dependency."
|
|
173
|
+
),
|
|
174
|
+
exportVersion: z2.string().describe("Version found in exported project's package.json."),
|
|
175
|
+
userVersion: z2.string().describe("Version currently present in user's package.json.")
|
|
176
|
+
});
|
|
46
177
|
var TRIGGER_PHRASES = [
|
|
47
178
|
"import page from PrimeUI",
|
|
48
179
|
"add page from PrimeUI",
|
|
@@ -59,8 +190,8 @@ WORKFLOW ORDER (always follow this sequence):
|
|
|
59
190
|
2. Reconcile PrimeUI pages with local project pages/routes/paths, then confirm target pages with user
|
|
60
191
|
3. primeui_create_export -> generate export snapshot for agreed pages/analysis scope
|
|
61
192
|
4. primeui_download_export -> download to temp directory
|
|
62
|
-
5.
|
|
63
|
-
6.
|
|
193
|
+
5. primeui_copy_page -> copy one page safely (repeat for each selected page)
|
|
194
|
+
6. Reconcile integration points and resolve reported conflicts, if any
|
|
64
195
|
7. primeui_clear_temp -> cleanup temp files
|
|
65
196
|
`.trim();
|
|
66
197
|
var initialInstructions = `
|
|
@@ -82,16 +213,16 @@ CRITICAL RULES FOR PAGE IMPORT:
|
|
|
82
213
|
- Before creating export, ask user which pages to add/update (specific pages or all missing pages) and keep that choice in thread context.
|
|
83
214
|
- The downloaded export in .primeui/temp/ contains a full standalone Next.js project.
|
|
84
215
|
Do NOT copy it wholesale.
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
216
|
+
- Always call primeui_copy_page for each confirmed page instead of manual file copy.
|
|
217
|
+
- primeui_copy_page performs safe copy only:
|
|
218
|
+
- new files are copied,
|
|
219
|
+
- identical files are reported,
|
|
220
|
+
- conflicting files are NEVER overwritten and are returned with diff.
|
|
221
|
+
- primeui_copy_page may add only missing dependencies to user's package.json.
|
|
222
|
+
It never upgrades existing dependency versions and never runs dependency installation.
|
|
223
|
+
- After page copy operations, inspect integration points (navigation configs, link menus, route registries, page indexes).
|
|
91
224
|
- If integration pattern is obvious, apply it and explicitly mark it in the report with "\u26A0 integration update".
|
|
92
225
|
- 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
226
|
`.trim();
|
|
96
227
|
var toolGetProjectInfo = {
|
|
97
228
|
title: "PrimeUI Project Info",
|
|
@@ -115,16 +246,16 @@ AFTER CALLING:
|
|
|
115
246
|
${WORKFLOW_SUMMARY}`,
|
|
116
247
|
inputSchema: {},
|
|
117
248
|
outputSchema: {
|
|
118
|
-
projectId:
|
|
249
|
+
projectId: z2.string().describe(
|
|
119
250
|
"PrimeUI project identifier associated with the API key and current import session context."
|
|
120
251
|
),
|
|
121
|
-
projectName:
|
|
252
|
+
projectName: z2.string().describe(
|
|
122
253
|
"Human-readable PrimeUI project name for confirmations in chat."
|
|
123
254
|
),
|
|
124
|
-
metadata:
|
|
255
|
+
metadata: z2.record(z2.unknown()).describe(
|
|
125
256
|
"Additional project metadata from PrimeUI. May include project-description and other context fields."
|
|
126
257
|
),
|
|
127
|
-
pages:
|
|
258
|
+
pages: z2.array(pageSchema).describe(
|
|
128
259
|
"All pages visible for this project context. Filter by user request and isReadyToExport before creating export."
|
|
129
260
|
)
|
|
130
261
|
},
|
|
@@ -151,7 +282,7 @@ Do NOT use this tool as a substitute for primeui_create_export. Always create a
|
|
|
151
282
|
${WORKFLOW_SUMMARY}`,
|
|
152
283
|
inputSchema: {},
|
|
153
284
|
outputSchema: {
|
|
154
|
-
exports:
|
|
285
|
+
exports: z2.array(exportItemSchema).describe(
|
|
155
286
|
"Available exports sorted by API policy. Use item.id to download a specific prior export if user asks."
|
|
156
287
|
)
|
|
157
288
|
},
|
|
@@ -177,13 +308,13 @@ AFTER CALLING:
|
|
|
177
308
|
${WORKFLOW_SUMMARY}`,
|
|
178
309
|
inputSchema: {},
|
|
179
310
|
outputSchema: {
|
|
180
|
-
export:
|
|
181
|
-
id:
|
|
311
|
+
export: z2.object({
|
|
312
|
+
id: z2.string().describe(
|
|
182
313
|
"Freshly created export identifier. Use this as primeui_download_export input.id."
|
|
183
314
|
),
|
|
184
315
|
status: exportStatusSchema
|
|
185
316
|
}).describe("Created export summary."),
|
|
186
|
-
pages:
|
|
317
|
+
pages: z2.array(pageSchema).describe(
|
|
187
318
|
"Page snapshot associated with this export operation. Validate requested pages here before download/import."
|
|
188
319
|
)
|
|
189
320
|
},
|
|
@@ -200,45 +331,37 @@ var toolDownloadExport = {
|
|
|
200
331
|
|
|
201
332
|
Do NOT call this as the first step. Requires export ID from primeui_create_export. Start with primeui_get_project_info instead.
|
|
202
333
|
|
|
203
|
-
WHEN TO USE: Call this AFTER primeui_create_export has returned a completed export. Requires
|
|
334
|
+
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.
|
|
335
|
+
|
|
336
|
+
NOTE:
|
|
337
|
+
- Download of an export ID without local pages snapshot may fail.
|
|
338
|
+
- For previous exports, create a fresh export first when possible.
|
|
204
339
|
|
|
205
340
|
AFTER DOWNLOAD:
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
|
|
211
|
-
|
|
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.
|
|
341
|
+
- Use manifestPath from this response as the contract for page slug/path mapping in copy operations.
|
|
342
|
+
- For each selected page, call primeui_copy_page with:
|
|
343
|
+
- originPageSlug (required),
|
|
344
|
+
- actualPageSlug (optional, if route remap is needed).
|
|
345
|
+
- If there are unresolved conflicts in copy report, stop and ask the user.
|
|
346
|
+
- Only after all requested pages are copied and reconciled, call primeui_clear_temp.
|
|
227
347
|
|
|
228
348
|
${WORKFLOW_SUMMARY}`,
|
|
229
349
|
inputSchema: {
|
|
230
|
-
id:
|
|
350
|
+
id: z2.string().describe(
|
|
231
351
|
"Export identifier to download. Prefer export.id from primeui_create_export; use primeui_list_exports only on explicit user request."
|
|
232
352
|
)
|
|
233
353
|
},
|
|
234
354
|
outputSchema: {
|
|
235
|
-
exportId:
|
|
355
|
+
exportId: z2.string().describe(
|
|
236
356
|
"Downloaded export identifier. Should match the requested input.id for traceability."
|
|
237
357
|
),
|
|
238
|
-
projectPath:
|
|
358
|
+
projectPath: z2.string().describe(
|
|
239
359
|
"Absolute local path to extracted export root in .primeui/temp/exports/[exportId]. Read files from here only."
|
|
240
360
|
),
|
|
241
|
-
|
|
361
|
+
manifestPath: z2.string().describe(
|
|
362
|
+
"Absolute path to sidecar export manifest file (.primeui/temp/exports/[exportId].manifest.json). primeui_copy_page depends on this file."
|
|
363
|
+
),
|
|
364
|
+
pages: z2.array(pageSchema).describe(
|
|
242
365
|
"Page descriptors for import decisions. Use pagePath/componentsPath from each item when copying code into user project."
|
|
243
366
|
)
|
|
244
367
|
},
|
|
@@ -249,6 +372,77 @@ MANUAL IMPORT STEPS per page (do not skip):
|
|
|
249
372
|
openWorldHint: true
|
|
250
373
|
}
|
|
251
374
|
};
|
|
375
|
+
var toolCopyPage = {
|
|
376
|
+
title: "PrimeUI Copy Page",
|
|
377
|
+
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.
|
|
378
|
+
|
|
379
|
+
WHEN TO USE:
|
|
380
|
+
- Call this AFTER primeui_download_export.
|
|
381
|
+
- Call once per selected page.
|
|
382
|
+
- Use actualPageSlug only when user wants to remap route during import.
|
|
383
|
+
|
|
384
|
+
BEHAVIOR:
|
|
385
|
+
- Reads export sidecar manifest to resolve originPageSlug -> pagePath/componentsPath.
|
|
386
|
+
- Copies page file + full page components folder + recursively discovered internal dependencies.
|
|
387
|
+
- Never overwrites existing conflicting files.
|
|
388
|
+
- Reports:
|
|
389
|
+
a) copied new files,
|
|
390
|
+
b) identical existing files,
|
|
391
|
+
c) conflicting files with diff,
|
|
392
|
+
d) missing dependencies added to package.json,
|
|
393
|
+
e) dependency version conflicts (report-only, no auto upgrade).
|
|
394
|
+
- Adds only missing dependencies to package.json (dependencies/devDependencies/peerDependencies).
|
|
395
|
+
- Never updates existing dependency versions and never runs install commands.
|
|
396
|
+
|
|
397
|
+
${WORKFLOW_SUMMARY}`,
|
|
398
|
+
inputSchema: {
|
|
399
|
+
originPageSlug: z2.string().describe(
|
|
400
|
+
"Source page slug from PrimeUI project pages list, for example '/', '/pricing', '/docs'."
|
|
401
|
+
),
|
|
402
|
+
actualPageSlug: z2.string().optional().describe(
|
|
403
|
+
"Optional destination slug in user's project. If omitted, originPageSlug is used."
|
|
404
|
+
)
|
|
405
|
+
},
|
|
406
|
+
outputSchema: {
|
|
407
|
+
exportId: z2.string().describe("Resolved local export identifier used for copy operation."),
|
|
408
|
+
exportPath: z2.string().describe("Absolute path to local extracted export project root."),
|
|
409
|
+
originPageSlug: z2.string().describe("Normalized source page slug requested for copy."),
|
|
410
|
+
actualPageSlug: z2.string().describe("Normalized destination slug used for target route paths."),
|
|
411
|
+
sourcePagePath: z2.string().describe("Source page path relative to export root."),
|
|
412
|
+
targetPagePath: z2.string().describe("Destination page path relative to user project root."),
|
|
413
|
+
sourceComponentsPath: z2.string().describe("Source components directory relative to export root."),
|
|
414
|
+
targetComponentsPath: z2.string().describe(
|
|
415
|
+
"Destination components directory relative to user project root."
|
|
416
|
+
),
|
|
417
|
+
newFiles: z2.array(fileTransferSchema).describe("New files copied into user project without conflicts."),
|
|
418
|
+
identicalFiles: z2.array(fileTransferSchema).describe(
|
|
419
|
+
"Existing files in user project that are byte-identical to export files."
|
|
420
|
+
),
|
|
421
|
+
conflictFiles: z2.array(conflictFileSchema).describe(
|
|
422
|
+
"Files that already exist and differ from export version. Never overwritten."
|
|
423
|
+
),
|
|
424
|
+
addedDependencies: z2.array(dependencyToAddSchema).describe(
|
|
425
|
+
"Dependencies that were missing and have been added to user's package.json."
|
|
426
|
+
),
|
|
427
|
+
dependenciesVersionConflicts: z2.array(dependencyVersionConflictSchema).describe(
|
|
428
|
+
"Dependencies with version mismatch between export and user project. Report only."
|
|
429
|
+
),
|
|
430
|
+
summary: z2.object({
|
|
431
|
+
totalCandidateFiles: z2.number().describe("Total number of candidate files considered for page copy."),
|
|
432
|
+
copiedFiles: z2.number().describe("Number of files copied as new."),
|
|
433
|
+
identicalFiles: z2.number().describe("Number of files detected as identical."),
|
|
434
|
+
conflictFiles: z2.number().describe("Number of conflicting files not copied."),
|
|
435
|
+
addedDependencies: z2.number().describe("Count of dependencies added to package.json."),
|
|
436
|
+
dependenciesVersionConflicts: z2.number().describe("Count of dependency version mismatches reported.")
|
|
437
|
+
})
|
|
438
|
+
},
|
|
439
|
+
annotations: {
|
|
440
|
+
readOnlyHint: false,
|
|
441
|
+
destructiveHint: false,
|
|
442
|
+
idempotentHint: false,
|
|
443
|
+
openWorldHint: false
|
|
444
|
+
}
|
|
445
|
+
};
|
|
252
446
|
var toolClearTemp = {
|
|
253
447
|
title: "PrimeUI Clear Temp",
|
|
254
448
|
description: `Delete all files in .primeui/temp/ and recreate empty temp structure.
|
|
@@ -262,7 +456,7 @@ Safe to call multiple times. Only affects .primeui/temp/ directory - never touch
|
|
|
262
456
|
${WORKFLOW_SUMMARY}`,
|
|
263
457
|
inputSchema: {},
|
|
264
458
|
outputSchema: {
|
|
265
|
-
success:
|
|
459
|
+
success: z2.boolean().describe(
|
|
266
460
|
"True when temp cleanup completed and baseline temp structure was recreated."
|
|
267
461
|
)
|
|
268
462
|
},
|
|
@@ -358,15 +552,31 @@ function createPrimeUiMcpServer(source) {
|
|
|
358
552
|
}
|
|
359
553
|
}
|
|
360
554
|
);
|
|
555
|
+
server.registerTool(
|
|
556
|
+
"primeui_copy_page",
|
|
557
|
+
toolCopyPage,
|
|
558
|
+
async ({ originPageSlug, actualPageSlug }) => {
|
|
559
|
+
try {
|
|
560
|
+
const result = await source.copyPage(
|
|
561
|
+
originPageSlug,
|
|
562
|
+
actualPageSlug
|
|
563
|
+
);
|
|
564
|
+
return okResult("primeui_copy_page", result);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
return errorResult(error);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
);
|
|
361
570
|
return server;
|
|
362
571
|
}
|
|
363
572
|
|
|
364
573
|
// src/services/project-sync-service.ts
|
|
365
|
-
import
|
|
574
|
+
import path5 from "path";
|
|
575
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
366
576
|
|
|
367
577
|
// src/lib/fs.ts
|
|
368
578
|
import { mkdir, rm, writeFile } from "fs/promises";
|
|
369
|
-
import
|
|
579
|
+
import path2 from "path";
|
|
370
580
|
import extractZipArchive from "extract-zip";
|
|
371
581
|
async function ensureDir(dirPath) {
|
|
372
582
|
await mkdir(dirPath, { recursive: true });
|
|
@@ -376,7 +586,7 @@ async function resetDir(dirPath) {
|
|
|
376
586
|
await mkdir(dirPath, { recursive: true });
|
|
377
587
|
}
|
|
378
588
|
async function writeUtf8(filePath, content) {
|
|
379
|
-
await ensureDir(
|
|
589
|
+
await ensureDir(path2.dirname(filePath));
|
|
380
590
|
await writeFile(filePath, content, "utf-8");
|
|
381
591
|
}
|
|
382
592
|
async function extractZip(zipPath, targetDir) {
|
|
@@ -389,7 +599,878 @@ async function extractZip(zipPath, targetDir) {
|
|
|
389
599
|
}
|
|
390
600
|
}
|
|
391
601
|
|
|
602
|
+
// src/services/page-copy-service.ts
|
|
603
|
+
import { readFile as readFile3, readdir, stat as stat3, writeFile as writeFile2 } from "fs/promises";
|
|
604
|
+
import path4 from "path";
|
|
605
|
+
|
|
606
|
+
// src/lib/import-graph.ts
|
|
607
|
+
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
608
|
+
import { builtinModules } from "module";
|
|
609
|
+
import path3 from "path";
|
|
610
|
+
var INTERNAL_EXTENSIONS = [
|
|
611
|
+
".ts",
|
|
612
|
+
".tsx",
|
|
613
|
+
".js",
|
|
614
|
+
".jsx",
|
|
615
|
+
".mjs",
|
|
616
|
+
".cjs",
|
|
617
|
+
".json"
|
|
618
|
+
];
|
|
619
|
+
var PARSEABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
620
|
+
".ts",
|
|
621
|
+
".tsx",
|
|
622
|
+
".js",
|
|
623
|
+
".jsx",
|
|
624
|
+
".mjs",
|
|
625
|
+
".cjs"
|
|
626
|
+
]);
|
|
627
|
+
var BUILTIN_MODULES = /* @__PURE__ */ new Set([
|
|
628
|
+
...builtinModules,
|
|
629
|
+
...builtinModules.map((item) => `node:${item}`)
|
|
630
|
+
]);
|
|
631
|
+
var STATIC_IMPORT_RE = /\bimport\s+(?:type\s+)?(?:[\s\S]*?\sfrom\s+)?["']([^"']+)["']/g;
|
|
632
|
+
var EXPORT_FROM_RE = /\bexport\s+(?:[\s\S]*?\sfrom\s+)["']([^"']+)["']/g;
|
|
633
|
+
var DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
634
|
+
var REQUIRE_RE = /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
635
|
+
function collectSpecifiers(content) {
|
|
636
|
+
const results = /* @__PURE__ */ new Set();
|
|
637
|
+
let match = null;
|
|
638
|
+
for (const re of [
|
|
639
|
+
STATIC_IMPORT_RE,
|
|
640
|
+
EXPORT_FROM_RE,
|
|
641
|
+
DYNAMIC_IMPORT_RE,
|
|
642
|
+
REQUIRE_RE
|
|
643
|
+
]) {
|
|
644
|
+
re.lastIndex = 0;
|
|
645
|
+
while ((match = re.exec(content)) !== null) {
|
|
646
|
+
const specifier = match[1]?.trim();
|
|
647
|
+
if (specifier) {
|
|
648
|
+
results.add(specifier);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return [...results];
|
|
653
|
+
}
|
|
654
|
+
function getExternalPackageName(specifier) {
|
|
655
|
+
if (!specifier || specifier.startsWith(".") || specifier.startsWith("/")) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
if (specifier.startsWith("node:")) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
if (specifier.startsWith("@/") || specifier.startsWith("@root/")) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
const parts = specifier.split("/");
|
|
665
|
+
if (parts.length === 0) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
if (specifier.startsWith("@")) {
|
|
669
|
+
if (parts.length < 2) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
return `${parts[0]}/${parts[1]}`;
|
|
673
|
+
}
|
|
674
|
+
return parts[0] ?? null;
|
|
675
|
+
}
|
|
676
|
+
async function pathExists(filePath) {
|
|
677
|
+
try {
|
|
678
|
+
await stat2(filePath);
|
|
679
|
+
return true;
|
|
680
|
+
} catch {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async function resolveFileCandidate(candidateBase) {
|
|
685
|
+
const ext = path3.extname(candidateBase);
|
|
686
|
+
if (ext) {
|
|
687
|
+
if (await pathExists(candidateBase)) {
|
|
688
|
+
return candidateBase;
|
|
689
|
+
}
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
for (const extension of INTERNAL_EXTENSIONS) {
|
|
693
|
+
const withExtension = `${candidateBase}${extension}`;
|
|
694
|
+
if (await pathExists(withExtension)) {
|
|
695
|
+
return withExtension;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (await pathExists(candidateBase)) {
|
|
699
|
+
const stats = await stat2(candidateBase);
|
|
700
|
+
if (stats.isFile()) {
|
|
701
|
+
return candidateBase;
|
|
702
|
+
}
|
|
703
|
+
if (stats.isDirectory()) {
|
|
704
|
+
for (const extension of INTERNAL_EXTENSIONS) {
|
|
705
|
+
const indexCandidate = path3.join(candidateBase, `index${extension}`);
|
|
706
|
+
if (await pathExists(indexCandidate)) {
|
|
707
|
+
return indexCandidate;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
async function resolveImportToFile(projectRoot, importerFilePath, specifier) {
|
|
715
|
+
const externalPackageName = getExternalPackageName(specifier);
|
|
716
|
+
if (externalPackageName) {
|
|
717
|
+
if (!BUILTIN_MODULES.has(externalPackageName)) {
|
|
718
|
+
return {
|
|
719
|
+
kind: "external",
|
|
720
|
+
packageName: externalPackageName
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
return { kind: "unknown" };
|
|
724
|
+
}
|
|
725
|
+
let candidateBase = null;
|
|
726
|
+
if (specifier.startsWith("@/")) {
|
|
727
|
+
candidateBase = path3.join(projectRoot, "src", specifier.slice(2));
|
|
728
|
+
} else if (specifier.startsWith("@root/")) {
|
|
729
|
+
candidateBase = path3.join(projectRoot, specifier.slice(6));
|
|
730
|
+
} else if (specifier.startsWith(".")) {
|
|
731
|
+
candidateBase = path3.resolve(path3.dirname(importerFilePath), specifier);
|
|
732
|
+
} else if (specifier.startsWith("/")) {
|
|
733
|
+
candidateBase = path3.join(projectRoot, specifier.slice(1));
|
|
734
|
+
} else {
|
|
735
|
+
return { kind: "unknown" };
|
|
736
|
+
}
|
|
737
|
+
const resolved = await resolveFileCandidate(candidateBase);
|
|
738
|
+
if (!resolved) {
|
|
739
|
+
return { kind: "unknown" };
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
kind: "internal",
|
|
743
|
+
filePath: resolved
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function shouldParseFile(filePath) {
|
|
747
|
+
return PARSEABLE_EXTENSIONS.has(path3.extname(filePath).toLowerCase());
|
|
748
|
+
}
|
|
749
|
+
async function buildImportGraph(input) {
|
|
750
|
+
const projectRoot = path3.resolve(input.projectRoot);
|
|
751
|
+
const queue = input.entryFiles.map((filePath) => path3.resolve(filePath));
|
|
752
|
+
const visited = /* @__PURE__ */ new Set();
|
|
753
|
+
const internalFiles = /* @__PURE__ */ new Set();
|
|
754
|
+
const externalPackages = /* @__PURE__ */ new Set();
|
|
755
|
+
while (queue.length > 0) {
|
|
756
|
+
const currentFilePath = queue.shift();
|
|
757
|
+
if (!currentFilePath || visited.has(currentFilePath)) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
visited.add(currentFilePath);
|
|
761
|
+
internalFiles.add(currentFilePath);
|
|
762
|
+
if (!shouldParseFile(currentFilePath)) {
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
let content = "";
|
|
766
|
+
try {
|
|
767
|
+
content = await readFile2(currentFilePath, "utf-8");
|
|
768
|
+
} catch {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const specifiers = collectSpecifiers(content);
|
|
772
|
+
for (const specifier of specifiers) {
|
|
773
|
+
const resolved = await resolveImportToFile(
|
|
774
|
+
projectRoot,
|
|
775
|
+
currentFilePath,
|
|
776
|
+
specifier
|
|
777
|
+
);
|
|
778
|
+
if (resolved.kind === "internal") {
|
|
779
|
+
if (!visited.has(resolved.filePath)) {
|
|
780
|
+
queue.push(resolved.filePath);
|
|
781
|
+
}
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (resolved.kind === "external") {
|
|
785
|
+
externalPackages.add(resolved.packageName);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
internalFiles: [...internalFiles].sort(),
|
|
791
|
+
externalPackages: [...externalPackages].sort()
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/lib/page-paths.ts
|
|
796
|
+
var WEBSITE_APP_ROOT = "src/app/(website)";
|
|
797
|
+
var PAGE_COMPONENTS_ROOT = "src/components/pages";
|
|
798
|
+
var normalizeSlug = (slug) => {
|
|
799
|
+
const trimmed = slug.trim();
|
|
800
|
+
if (!trimmed || trimmed === "/") {
|
|
801
|
+
return "/";
|
|
802
|
+
}
|
|
803
|
+
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
804
|
+
const withoutTrailingSlash = withLeadingSlash.replace(/\/+$/, "");
|
|
805
|
+
return withoutTrailingSlash || "/";
|
|
806
|
+
};
|
|
807
|
+
var slugToFolderPath = (normalizedSlug) => {
|
|
808
|
+
if (normalizedSlug === "/") {
|
|
809
|
+
return "";
|
|
810
|
+
}
|
|
811
|
+
return normalizedSlug.slice(1);
|
|
812
|
+
};
|
|
813
|
+
var toPathSegment = (value) => {
|
|
814
|
+
const normalized = value.trim().toLowerCase().replace(/[^a-z0-9/-]+/g, "-");
|
|
815
|
+
const compact = normalized.replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
816
|
+
return compact || "page";
|
|
817
|
+
};
|
|
818
|
+
var buildWebsitePagePath = (normalizedSlug) => {
|
|
819
|
+
if (normalizedSlug === "/") {
|
|
820
|
+
return `${WEBSITE_APP_ROOT}/page.tsx`;
|
|
821
|
+
}
|
|
822
|
+
return `${WEBSITE_APP_ROOT}${normalizedSlug}/page.tsx`;
|
|
823
|
+
};
|
|
824
|
+
var buildComponentsPath = (folderPath) => `${PAGE_COMPONENTS_ROOT}/${folderPath}`;
|
|
825
|
+
var resolveBlogSectionRoot = (slugFolderPath) => {
|
|
826
|
+
if (!slugFolderPath) {
|
|
827
|
+
return "blog";
|
|
828
|
+
}
|
|
829
|
+
const segments = slugFolderPath.split("/").filter(Boolean);
|
|
830
|
+
if (segments.length <= 1) {
|
|
831
|
+
return "blog";
|
|
832
|
+
}
|
|
833
|
+
return segments.slice(0, -1).join("/");
|
|
834
|
+
};
|
|
835
|
+
function resolvePrimeUiPageExportPaths({
|
|
836
|
+
pageType,
|
|
837
|
+
slug
|
|
838
|
+
}) {
|
|
839
|
+
const normalizedSlug = normalizeSlug(slug);
|
|
840
|
+
const slugFolderPath = slugToFolderPath(normalizedSlug);
|
|
841
|
+
switch (pageType) {
|
|
842
|
+
case "landing": {
|
|
843
|
+
const componentsFolder = slugFolderPath || "home";
|
|
844
|
+
return {
|
|
845
|
+
pagePath: buildWebsitePagePath(normalizedSlug),
|
|
846
|
+
componentsPath: buildComponentsPath(componentsFolder)
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
case "pricing":
|
|
850
|
+
case "contact-us": {
|
|
851
|
+
const componentsFolder = slugFolderPath || pageType;
|
|
852
|
+
return {
|
|
853
|
+
pagePath: buildWebsitePagePath(normalizedSlug),
|
|
854
|
+
componentsPath: buildComponentsPath(componentsFolder)
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
case "docs": {
|
|
858
|
+
const docsFolder = slugFolderPath || "docs";
|
|
859
|
+
return {
|
|
860
|
+
pagePath: `${WEBSITE_APP_ROOT}/${docsFolder}/[[...slug]]/page.tsx`,
|
|
861
|
+
componentsPath: buildComponentsPath("docs")
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
case "blogIndex": {
|
|
865
|
+
const blogFolder = slugFolderPath || "blog";
|
|
866
|
+
return {
|
|
867
|
+
pagePath: `${WEBSITE_APP_ROOT}/${blogFolder}/page.tsx`,
|
|
868
|
+
componentsPath: buildComponentsPath("blog")
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
case "blogPost": {
|
|
872
|
+
const blogSectionRoot = resolveBlogSectionRoot(slugFolderPath);
|
|
873
|
+
return {
|
|
874
|
+
pagePath: `${WEBSITE_APP_ROOT}/${blogSectionRoot}/[slug]/page.tsx`,
|
|
875
|
+
componentsPath: buildComponentsPath("blog")
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
case "legal":
|
|
879
|
+
return {
|
|
880
|
+
pagePath: buildWebsitePagePath(normalizedSlug),
|
|
881
|
+
componentsPath: buildComponentsPath("legal")
|
|
882
|
+
};
|
|
883
|
+
default: {
|
|
884
|
+
const fallbackFolder = slugFolderPath || toPathSegment(pageType);
|
|
885
|
+
return {
|
|
886
|
+
pagePath: buildWebsitePagePath(normalizedSlug),
|
|
887
|
+
componentsPath: buildComponentsPath(fallbackFolder)
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// src/lib/text-diff.ts
|
|
894
|
+
var DEFAULT_MAX_DIFF_CHARS = 16e3;
|
|
895
|
+
var MAX_LCS_CELLS = 2e6;
|
|
896
|
+
function computeOps(oldLines, newLines) {
|
|
897
|
+
const oldLength = oldLines.length;
|
|
898
|
+
const newLength = newLines.length;
|
|
899
|
+
const table = Array.from(
|
|
900
|
+
{ length: oldLength + 1 },
|
|
901
|
+
() => Array(newLength + 1).fill(0)
|
|
902
|
+
);
|
|
903
|
+
for (let i2 = oldLength - 1; i2 >= 0; i2 -= 1) {
|
|
904
|
+
for (let j2 = newLength - 1; j2 >= 0; j2 -= 1) {
|
|
905
|
+
if (oldLines[i2] === newLines[j2]) {
|
|
906
|
+
table[i2][j2] = table[i2 + 1][j2 + 1] + 1;
|
|
907
|
+
} else {
|
|
908
|
+
table[i2][j2] = Math.max(table[i2 + 1][j2], table[i2][j2 + 1]);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const ops = [];
|
|
913
|
+
let i = 0;
|
|
914
|
+
let j = 0;
|
|
915
|
+
while (i < oldLength && j < newLength) {
|
|
916
|
+
if (oldLines[i] === newLines[j]) {
|
|
917
|
+
ops.push({ kind: "equal", value: oldLines[i] ?? "" });
|
|
918
|
+
i += 1;
|
|
919
|
+
j += 1;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if (table[i + 1][j] >= table[i][j + 1]) {
|
|
923
|
+
ops.push({ kind: "delete", value: oldLines[i] ?? "" });
|
|
924
|
+
i += 1;
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
ops.push({ kind: "insert", value: newLines[j] ?? "" });
|
|
928
|
+
j += 1;
|
|
929
|
+
}
|
|
930
|
+
while (i < oldLength) {
|
|
931
|
+
ops.push({ kind: "delete", value: oldLines[i] ?? "" });
|
|
932
|
+
i += 1;
|
|
933
|
+
}
|
|
934
|
+
while (j < newLength) {
|
|
935
|
+
ops.push({ kind: "insert", value: newLines[j] ?? "" });
|
|
936
|
+
j += 1;
|
|
937
|
+
}
|
|
938
|
+
return ops;
|
|
939
|
+
}
|
|
940
|
+
function buildUnifiedDiff(input) {
|
|
941
|
+
const maxChars = input.maxChars ?? DEFAULT_MAX_DIFF_CHARS;
|
|
942
|
+
if (input.oldText === input.newText) {
|
|
943
|
+
return "";
|
|
944
|
+
}
|
|
945
|
+
const oldLines = input.oldText.split(/\r?\n/);
|
|
946
|
+
const newLines = input.newText.split(/\r?\n/);
|
|
947
|
+
const cells = (oldLines.length + 1) * (newLines.length + 1);
|
|
948
|
+
if (cells > MAX_LCS_CELLS) {
|
|
949
|
+
return [
|
|
950
|
+
`--- ${input.oldLabel}`,
|
|
951
|
+
`+++ ${input.newLabel}`,
|
|
952
|
+
"@@ diff omitted @@",
|
|
953
|
+
"Diff is too large to render safely."
|
|
954
|
+
].join("\n");
|
|
955
|
+
}
|
|
956
|
+
const ops = computeOps(oldLines, newLines);
|
|
957
|
+
const output = [
|
|
958
|
+
`--- ${input.oldLabel}`,
|
|
959
|
+
`+++ ${input.newLabel}`,
|
|
960
|
+
`@@ -1,${oldLines.length} +1,${newLines.length} @@`
|
|
961
|
+
];
|
|
962
|
+
for (const op of ops) {
|
|
963
|
+
if (op.kind === "equal") {
|
|
964
|
+
output.push(` ${op.value}`);
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
if (op.kind === "delete") {
|
|
968
|
+
output.push(`-${op.value}`);
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
output.push(`+${op.value}`);
|
|
972
|
+
}
|
|
973
|
+
const diff = output.join("\n");
|
|
974
|
+
if (diff.length <= maxChars) {
|
|
975
|
+
return diff;
|
|
976
|
+
}
|
|
977
|
+
return `${diff.slice(0, maxChars)}
|
|
978
|
+
... [diff truncated]`;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/services/page-copy-service.ts
|
|
982
|
+
var DEPENDENCY_SECTIONS = [
|
|
983
|
+
"dependencies",
|
|
984
|
+
"devDependencies",
|
|
985
|
+
"peerDependencies"
|
|
986
|
+
];
|
|
987
|
+
var REWRITABLE_IMPORT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
988
|
+
".ts",
|
|
989
|
+
".tsx",
|
|
990
|
+
".js",
|
|
991
|
+
".jsx",
|
|
992
|
+
".mjs",
|
|
993
|
+
".cjs"
|
|
994
|
+
]);
|
|
995
|
+
function normalizeSlug2(slug) {
|
|
996
|
+
const trimmed = slug.trim();
|
|
997
|
+
if (!trimmed || trimmed === "/") {
|
|
998
|
+
return "/";
|
|
999
|
+
}
|
|
1000
|
+
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
1001
|
+
return withLeadingSlash.replace(/\/+$/, "") || "/";
|
|
1002
|
+
}
|
|
1003
|
+
function toPosixPath(value) {
|
|
1004
|
+
return value.split(path4.sep).join("/");
|
|
1005
|
+
}
|
|
1006
|
+
function toProjectRelative(rootPath, absolutePath) {
|
|
1007
|
+
return toPosixPath(path4.relative(rootPath, absolutePath));
|
|
1008
|
+
}
|
|
1009
|
+
function escapeRegExp(value) {
|
|
1010
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1011
|
+
}
|
|
1012
|
+
function isBinaryBuffer(buffer) {
|
|
1013
|
+
const limit = Math.min(buffer.length, 8e3);
|
|
1014
|
+
for (let index = 0; index < limit; index += 1) {
|
|
1015
|
+
if (buffer[index] === 0) {
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
function stripSrcPrefix(relativePath) {
|
|
1022
|
+
return relativePath.replace(/^src\//, "");
|
|
1023
|
+
}
|
|
1024
|
+
function buildImportRewritePlan(sourceComponentsPath, targetComponentsPath) {
|
|
1025
|
+
const normalizedSource = toPosixPath(sourceComponentsPath);
|
|
1026
|
+
const normalizedTarget = toPosixPath(targetComponentsPath);
|
|
1027
|
+
if (normalizedSource === normalizedTarget) {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
const sourceAtAliasPrefix = `@/${stripSrcPrefix(normalizedSource)}`;
|
|
1031
|
+
const targetAtAliasPrefix = `@/${stripSrcPrefix(normalizedTarget)}`;
|
|
1032
|
+
const sourceRootAliasPrefix = `@root/${normalizedSource}`;
|
|
1033
|
+
const targetRootAliasPrefix = `@root/${normalizedTarget}`;
|
|
1034
|
+
return {
|
|
1035
|
+
sourceAtAliasPrefix,
|
|
1036
|
+
targetAtAliasPrefix,
|
|
1037
|
+
sourceRootAliasPrefix,
|
|
1038
|
+
targetRootAliasPrefix
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function replaceAliasPrefixInImportStrings(content, sourcePrefix, targetPrefix) {
|
|
1042
|
+
const pattern = new RegExp(
|
|
1043
|
+
`(["'\`])${escapeRegExp(sourcePrefix)}(?=\\/|\\1)`,
|
|
1044
|
+
"g"
|
|
1045
|
+
);
|
|
1046
|
+
return content.replace(pattern, `$1${targetPrefix}`);
|
|
1047
|
+
}
|
|
1048
|
+
function rewriteImportsForRemappedSlug(content, plan) {
|
|
1049
|
+
const afterAtAlias = replaceAliasPrefixInImportStrings(
|
|
1050
|
+
content,
|
|
1051
|
+
plan.sourceAtAliasPrefix,
|
|
1052
|
+
plan.targetAtAliasPrefix
|
|
1053
|
+
);
|
|
1054
|
+
return replaceAliasPrefixInImportStrings(
|
|
1055
|
+
afterAtAlias,
|
|
1056
|
+
plan.sourceRootAliasPrefix,
|
|
1057
|
+
plan.targetRootAliasPrefix
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
function buildPlannedSourceBuffer(sourceBuffer, sourceFilePath, importRewritePlan) {
|
|
1061
|
+
if (!importRewritePlan) {
|
|
1062
|
+
return sourceBuffer;
|
|
1063
|
+
}
|
|
1064
|
+
const extension = path4.extname(sourceFilePath).toLowerCase();
|
|
1065
|
+
if (!REWRITABLE_IMPORT_EXTENSIONS.has(extension)) {
|
|
1066
|
+
return sourceBuffer;
|
|
1067
|
+
}
|
|
1068
|
+
if (isBinaryBuffer(sourceBuffer)) {
|
|
1069
|
+
return sourceBuffer;
|
|
1070
|
+
}
|
|
1071
|
+
const sourceText = sourceBuffer.toString("utf-8");
|
|
1072
|
+
const rewritten = rewriteImportsForRemappedSlug(
|
|
1073
|
+
sourceText,
|
|
1074
|
+
importRewritePlan
|
|
1075
|
+
);
|
|
1076
|
+
if (rewritten === sourceText) {
|
|
1077
|
+
return sourceBuffer;
|
|
1078
|
+
}
|
|
1079
|
+
return Buffer.from(rewritten, "utf-8");
|
|
1080
|
+
}
|
|
1081
|
+
async function readJsonFile(filePath) {
|
|
1082
|
+
const content = await readFile3(filePath, "utf-8");
|
|
1083
|
+
return JSON.parse(content);
|
|
1084
|
+
}
|
|
1085
|
+
async function fileExists2(filePath) {
|
|
1086
|
+
try {
|
|
1087
|
+
await stat3(filePath);
|
|
1088
|
+
return true;
|
|
1089
|
+
} catch {
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function isPageRecord(value) {
|
|
1094
|
+
if (!value || typeof value !== "object") {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
const maybe = value;
|
|
1098
|
+
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";
|
|
1099
|
+
}
|
|
1100
|
+
function parseManifest(value) {
|
|
1101
|
+
if (!value || typeof value !== "object") {
|
|
1102
|
+
throw new Error("Invalid export manifest format.");
|
|
1103
|
+
}
|
|
1104
|
+
const maybe = value;
|
|
1105
|
+
if (maybe.schemaVersion !== 1 || typeof maybe.generatedAt !== "string" || typeof maybe.exportId !== "string" || typeof maybe.projectPath !== "string" || !Array.isArray(maybe.pages)) {
|
|
1106
|
+
throw new Error("Export manifest does not match expected schema.");
|
|
1107
|
+
}
|
|
1108
|
+
if (!maybe.pages.every(isPageRecord)) {
|
|
1109
|
+
throw new Error("Export manifest pages payload is invalid.");
|
|
1110
|
+
}
|
|
1111
|
+
return {
|
|
1112
|
+
schemaVersion: 1,
|
|
1113
|
+
generatedAt: maybe.generatedAt,
|
|
1114
|
+
exportId: maybe.exportId,
|
|
1115
|
+
projectPath: maybe.projectPath,
|
|
1116
|
+
pages: maybe.pages
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
async function listFilesRecursively(dirPath) {
|
|
1120
|
+
const files = [];
|
|
1121
|
+
const stack = [dirPath];
|
|
1122
|
+
while (stack.length > 0) {
|
|
1123
|
+
const current = stack.pop();
|
|
1124
|
+
if (!current) {
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
1128
|
+
for (const entry of entries) {
|
|
1129
|
+
const absolutePath = path4.join(current, entry.name);
|
|
1130
|
+
if (entry.isDirectory()) {
|
|
1131
|
+
stack.push(absolutePath);
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
if (entry.isFile()) {
|
|
1135
|
+
files.push(absolutePath);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return files;
|
|
1140
|
+
}
|
|
1141
|
+
async function resolveSingleExportDirectory(exportsRoot) {
|
|
1142
|
+
const entries = await readdir(exportsRoot, { withFileTypes: true });
|
|
1143
|
+
const exportDirectories = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
|
|
1144
|
+
if (exportDirectories.length === 0) {
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
"No downloaded exports found in .primeui/temp/exports. Run primeui_download_export first."
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
if (exportDirectories.length > 1) {
|
|
1150
|
+
throw new Error(
|
|
1151
|
+
"Multiple export folders found in .primeui/temp/exports. Clear temp with primeui_clear_temp and download one export."
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
const exportId = exportDirectories[0] ?? "";
|
|
1155
|
+
return {
|
|
1156
|
+
exportId,
|
|
1157
|
+
exportPath: path4.join(exportsRoot, exportId)
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
function ensureSafeTargetPath(projectRoot, targetPath) {
|
|
1161
|
+
const relative = path4.relative(projectRoot, targetPath);
|
|
1162
|
+
if (relative.startsWith("..") || path4.isAbsolute(relative)) {
|
|
1163
|
+
throw new Error(
|
|
1164
|
+
`Refusing to write outside project root. Computed target: ${targetPath}`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
function getDependencyVersion(packageJson, packageName) {
|
|
1169
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
1170
|
+
const sectionData = packageJson[section];
|
|
1171
|
+
if (!sectionData || typeof sectionData !== "object") {
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
const value = sectionData[packageName];
|
|
1175
|
+
if (typeof value === "string") {
|
|
1176
|
+
return {
|
|
1177
|
+
section,
|
|
1178
|
+
version: value
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
function getOrCreateDependencySection(packageJson, section) {
|
|
1185
|
+
const existing = packageJson[section];
|
|
1186
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
|
1187
|
+
const created = {};
|
|
1188
|
+
packageJson[section] = created;
|
|
1189
|
+
return created;
|
|
1190
|
+
}
|
|
1191
|
+
return existing;
|
|
1192
|
+
}
|
|
1193
|
+
function sortDependencySections(packageJson) {
|
|
1194
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
1195
|
+
const sectionData = packageJson[section];
|
|
1196
|
+
if (!sectionData || typeof sectionData !== "object" || Array.isArray(sectionData)) {
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
const sorted = Object.fromEntries(
|
|
1200
|
+
Object.entries(sectionData).sort(
|
|
1201
|
+
([a], [b]) => a.localeCompare(b)
|
|
1202
|
+
)
|
|
1203
|
+
);
|
|
1204
|
+
packageJson[section] = sorted;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function resolveTargetFilePath(input) {
|
|
1208
|
+
const {
|
|
1209
|
+
sourceFilePath,
|
|
1210
|
+
sourcePagePath,
|
|
1211
|
+
sourceComponentsPath,
|
|
1212
|
+
targetPagePath,
|
|
1213
|
+
targetComponentsPath,
|
|
1214
|
+
exportRoot,
|
|
1215
|
+
projectRoot
|
|
1216
|
+
} = input;
|
|
1217
|
+
if (sourceFilePath === sourcePagePath) {
|
|
1218
|
+
return targetPagePath;
|
|
1219
|
+
}
|
|
1220
|
+
const componentsPrefix = `${sourceComponentsPath}${path4.sep}`;
|
|
1221
|
+
if (sourceFilePath === sourceComponentsPath || sourceFilePath.startsWith(componentsPrefix)) {
|
|
1222
|
+
const relativeToComponents = path4.relative(
|
|
1223
|
+
sourceComponentsPath,
|
|
1224
|
+
sourceFilePath
|
|
1225
|
+
);
|
|
1226
|
+
return path4.join(targetComponentsPath, relativeToComponents);
|
|
1227
|
+
}
|
|
1228
|
+
const relativeToExport = path4.relative(exportRoot, sourceFilePath);
|
|
1229
|
+
if (relativeToExport.startsWith("..") || path4.isAbsolute(relativeToExport)) {
|
|
1230
|
+
throw new Error(`Source file is outside export root: ${sourceFilePath}`);
|
|
1231
|
+
}
|
|
1232
|
+
return path4.join(projectRoot, relativeToExport);
|
|
1233
|
+
}
|
|
1234
|
+
async function copyPageFromExport(input) {
|
|
1235
|
+
const normalizedOriginSlug = normalizeSlug2(input.originPageSlug);
|
|
1236
|
+
const normalizedActualSlug = normalizeSlug2(
|
|
1237
|
+
input.actualPageSlug ?? input.originPageSlug
|
|
1238
|
+
);
|
|
1239
|
+
const isSlugRemapped = normalizedActualSlug !== normalizedOriginSlug;
|
|
1240
|
+
const { exportId, exportPath } = await resolveSingleExportDirectory(
|
|
1241
|
+
input.exportsRoot
|
|
1242
|
+
);
|
|
1243
|
+
const manifestPath = path4.join(
|
|
1244
|
+
input.exportsRoot,
|
|
1245
|
+
`${exportId}.manifest.json`
|
|
1246
|
+
);
|
|
1247
|
+
if (!await fileExists2(manifestPath)) {
|
|
1248
|
+
throw new Error(
|
|
1249
|
+
`Export manifest is missing at ${manifestPath}. Run primeui_download_export to regenerate it.`
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
const manifest = parseManifest(await readJsonFile(manifestPath));
|
|
1253
|
+
if (manifest.exportId !== exportId) {
|
|
1254
|
+
throw new Error(
|
|
1255
|
+
`Manifest exportId mismatch: expected ${exportId}, got ${manifest.exportId}. Re-download export.`
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
const page = manifest.pages.find(
|
|
1259
|
+
(item) => normalizeSlug2(item.slug) === normalizedOriginSlug
|
|
1260
|
+
);
|
|
1261
|
+
if (!page) {
|
|
1262
|
+
throw new Error(
|
|
1263
|
+
`Page not found in manifest for slug: ${normalizedOriginSlug}`
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
const sourcePagePath = path4.join(exportPath, page.pagePath);
|
|
1267
|
+
const sourceComponentsPath = path4.join(exportPath, page.componentsPath);
|
|
1268
|
+
const sourcePageStats = await stat3(sourcePagePath).catch(() => null);
|
|
1269
|
+
if (!sourcePageStats?.isFile()) {
|
|
1270
|
+
throw new Error(`Source page file not found: ${sourcePagePath}`);
|
|
1271
|
+
}
|
|
1272
|
+
const sourceComponentsStats = await stat3(sourceComponentsPath).catch(
|
|
1273
|
+
() => null
|
|
1274
|
+
);
|
|
1275
|
+
if (!sourceComponentsStats?.isDirectory()) {
|
|
1276
|
+
throw new Error(
|
|
1277
|
+
`Source components folder not found: ${sourceComponentsPath}`
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
const targetPaths = normalizedActualSlug === normalizedOriginSlug ? {
|
|
1281
|
+
pagePath: page.pagePath,
|
|
1282
|
+
componentsPath: page.componentsPath
|
|
1283
|
+
} : resolvePrimeUiPageExportPaths({
|
|
1284
|
+
pageType: page.pageType,
|
|
1285
|
+
slug: normalizedActualSlug
|
|
1286
|
+
});
|
|
1287
|
+
const targetPagePath = path4.join(input.projectRoot, targetPaths.pagePath);
|
|
1288
|
+
const targetComponentsPath = path4.join(
|
|
1289
|
+
input.projectRoot,
|
|
1290
|
+
targetPaths.componentsPath
|
|
1291
|
+
);
|
|
1292
|
+
const importRewritePlan = isSlugRemapped ? buildImportRewritePlan(page.componentsPath, targetPaths.componentsPath) : null;
|
|
1293
|
+
ensureSafeTargetPath(input.projectRoot, targetPagePath);
|
|
1294
|
+
ensureSafeTargetPath(input.projectRoot, targetComponentsPath);
|
|
1295
|
+
const componentFiles = await listFilesRecursively(sourceComponentsPath);
|
|
1296
|
+
const seedFiles = [sourcePagePath, ...componentFiles];
|
|
1297
|
+
const importGraph = await buildImportGraph({
|
|
1298
|
+
projectRoot: exportPath,
|
|
1299
|
+
entryFiles: seedFiles
|
|
1300
|
+
});
|
|
1301
|
+
const candidateFiles = [...new Set(importGraph.internalFiles)].sort(
|
|
1302
|
+
(a, b) => a.localeCompare(b)
|
|
1303
|
+
);
|
|
1304
|
+
const newFiles = [];
|
|
1305
|
+
const identicalFiles = [];
|
|
1306
|
+
const conflictFiles = [];
|
|
1307
|
+
for (const sourceFilePath of candidateFiles) {
|
|
1308
|
+
const targetFilePath = resolveTargetFilePath({
|
|
1309
|
+
sourceFilePath,
|
|
1310
|
+
sourcePagePath,
|
|
1311
|
+
sourceComponentsPath,
|
|
1312
|
+
targetPagePath,
|
|
1313
|
+
targetComponentsPath,
|
|
1314
|
+
exportRoot: exportPath,
|
|
1315
|
+
projectRoot: input.projectRoot
|
|
1316
|
+
});
|
|
1317
|
+
ensureSafeTargetPath(input.projectRoot, targetFilePath);
|
|
1318
|
+
const sourceBuffer = await readFile3(sourceFilePath);
|
|
1319
|
+
const plannedSourceBuffer = buildPlannedSourceBuffer(
|
|
1320
|
+
sourceBuffer,
|
|
1321
|
+
sourceFilePath,
|
|
1322
|
+
importRewritePlan
|
|
1323
|
+
);
|
|
1324
|
+
let targetBuffer = null;
|
|
1325
|
+
try {
|
|
1326
|
+
targetBuffer = await readFile3(targetFilePath);
|
|
1327
|
+
} catch {
|
|
1328
|
+
targetBuffer = null;
|
|
1329
|
+
}
|
|
1330
|
+
const sourceRelative = toProjectRelative(exportPath, sourceFilePath);
|
|
1331
|
+
const targetRelative = toProjectRelative(input.projectRoot, targetFilePath);
|
|
1332
|
+
if (!targetBuffer) {
|
|
1333
|
+
await ensureDir(path4.dirname(targetFilePath));
|
|
1334
|
+
await writeFile2(targetFilePath, plannedSourceBuffer);
|
|
1335
|
+
newFiles.push({
|
|
1336
|
+
sourcePath: sourceRelative,
|
|
1337
|
+
targetPath: targetRelative
|
|
1338
|
+
});
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
if (plannedSourceBuffer.equals(targetBuffer)) {
|
|
1342
|
+
identicalFiles.push({
|
|
1343
|
+
sourcePath: sourceRelative,
|
|
1344
|
+
targetPath: targetRelative
|
|
1345
|
+
});
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
const isBinary = isBinaryBuffer(plannedSourceBuffer) || isBinaryBuffer(targetBuffer);
|
|
1349
|
+
const diff = isBinary ? "Binary files differ." : buildUnifiedDiff({
|
|
1350
|
+
oldText: targetBuffer.toString("utf-8"),
|
|
1351
|
+
newText: plannedSourceBuffer.toString("utf-8"),
|
|
1352
|
+
oldLabel: `user/${targetRelative}`,
|
|
1353
|
+
newLabel: `export/${sourceRelative}`
|
|
1354
|
+
});
|
|
1355
|
+
conflictFiles.push({
|
|
1356
|
+
sourcePath: sourceRelative,
|
|
1357
|
+
targetPath: targetRelative,
|
|
1358
|
+
diff,
|
|
1359
|
+
isBinary
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
const exportPackageJsonPath = path4.join(exportPath, "package.json");
|
|
1363
|
+
const userPackageJsonPath = path4.join(input.projectRoot, "package.json");
|
|
1364
|
+
if (!await fileExists2(exportPackageJsonPath)) {
|
|
1365
|
+
throw new Error(`Export package.json not found: ${exportPackageJsonPath}`);
|
|
1366
|
+
}
|
|
1367
|
+
if (!await fileExists2(userPackageJsonPath)) {
|
|
1368
|
+
throw new Error(`User package.json not found: ${userPackageJsonPath}`);
|
|
1369
|
+
}
|
|
1370
|
+
const exportPackageJson = await readJsonFile(
|
|
1371
|
+
exportPackageJsonPath
|
|
1372
|
+
);
|
|
1373
|
+
const userPackageJson = await readJsonFile(userPackageJsonPath);
|
|
1374
|
+
const addedDependencies = [];
|
|
1375
|
+
const dependenciesVersionConflicts = [];
|
|
1376
|
+
for (const packageName of importGraph.externalPackages) {
|
|
1377
|
+
const exportDependency = getDependencyVersion(
|
|
1378
|
+
exportPackageJson,
|
|
1379
|
+
packageName
|
|
1380
|
+
);
|
|
1381
|
+
if (!exportDependency) {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
const userDependency = getDependencyVersion(userPackageJson, packageName);
|
|
1385
|
+
if (!userDependency) {
|
|
1386
|
+
const sectionData = getOrCreateDependencySection(
|
|
1387
|
+
userPackageJson,
|
|
1388
|
+
exportDependency.section
|
|
1389
|
+
);
|
|
1390
|
+
sectionData[packageName] = exportDependency.version;
|
|
1391
|
+
addedDependencies.push({
|
|
1392
|
+
packageName,
|
|
1393
|
+
version: exportDependency.version,
|
|
1394
|
+
section: exportDependency.section
|
|
1395
|
+
});
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (userDependency.version !== exportDependency.version) {
|
|
1399
|
+
dependenciesVersionConflicts.push({
|
|
1400
|
+
packageName,
|
|
1401
|
+
section: exportDependency.section,
|
|
1402
|
+
exportVersion: exportDependency.version,
|
|
1403
|
+
userVersion: userDependency.version
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
if (addedDependencies.length > 0) {
|
|
1408
|
+
sortDependencySections(userPackageJson);
|
|
1409
|
+
await writeFile2(
|
|
1410
|
+
userPackageJsonPath,
|
|
1411
|
+
`${JSON.stringify(userPackageJson, null, 2)}
|
|
1412
|
+
`,
|
|
1413
|
+
"utf-8"
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
return {
|
|
1417
|
+
exportId,
|
|
1418
|
+
exportPath,
|
|
1419
|
+
originPageSlug: normalizedOriginSlug,
|
|
1420
|
+
actualPageSlug: normalizedActualSlug,
|
|
1421
|
+
sourcePagePath: page.pagePath,
|
|
1422
|
+
targetPagePath: targetPaths.pagePath,
|
|
1423
|
+
sourceComponentsPath: page.componentsPath,
|
|
1424
|
+
targetComponentsPath: targetPaths.componentsPath,
|
|
1425
|
+
newFiles,
|
|
1426
|
+
identicalFiles,
|
|
1427
|
+
conflictFiles,
|
|
1428
|
+
addedDependencies: addedDependencies.sort(
|
|
1429
|
+
(a, b) => a.packageName.localeCompare(b.packageName)
|
|
1430
|
+
),
|
|
1431
|
+
dependenciesVersionConflicts: dependenciesVersionConflicts.sort(
|
|
1432
|
+
(a, b) => a.packageName.localeCompare(b.packageName)
|
|
1433
|
+
),
|
|
1434
|
+
summary: {
|
|
1435
|
+
totalCandidateFiles: candidateFiles.length,
|
|
1436
|
+
copiedFiles: newFiles.length,
|
|
1437
|
+
identicalFiles: identicalFiles.length,
|
|
1438
|
+
conflictFiles: conflictFiles.length,
|
|
1439
|
+
addedDependencies: addedDependencies.length,
|
|
1440
|
+
dependenciesVersionConflicts: dependenciesVersionConflicts.length
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
|
|
392
1445
|
// src/services/project-sync-service.ts
|
|
1446
|
+
function isProjectPage(value) {
|
|
1447
|
+
if (!value || typeof value !== "object") {
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
const maybe = value;
|
|
1451
|
+
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";
|
|
1452
|
+
}
|
|
1453
|
+
function buildPagesSnapshotPath(exportsRoot, exportId) {
|
|
1454
|
+
return path5.join(exportsRoot, `${exportId}.pages.snapshot.json`);
|
|
1455
|
+
}
|
|
1456
|
+
function parsePagesSnapshot(value) {
|
|
1457
|
+
if (!value || typeof value !== "object") {
|
|
1458
|
+
throw new Error("Export pages snapshot is invalid.");
|
|
1459
|
+
}
|
|
1460
|
+
const maybe = value;
|
|
1461
|
+
if (maybe.schemaVersion !== 1 || typeof maybe.generatedAt !== "string" || typeof maybe.exportId !== "string" || !Array.isArray(maybe.pages)) {
|
|
1462
|
+
throw new Error("Export pages snapshot does not match expected schema.");
|
|
1463
|
+
}
|
|
1464
|
+
if (!maybe.pages.every(isProjectPage)) {
|
|
1465
|
+
throw new Error("Export pages snapshot has invalid pages payload.");
|
|
1466
|
+
}
|
|
1467
|
+
return {
|
|
1468
|
+
schemaVersion: 1,
|
|
1469
|
+
generatedAt: maybe.generatedAt,
|
|
1470
|
+
exportId: maybe.exportId,
|
|
1471
|
+
pages: maybe.pages
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
393
1474
|
var ProjectSyncService = class {
|
|
394
1475
|
provider;
|
|
395
1476
|
projectRoot;
|
|
@@ -399,9 +1480,9 @@ var ProjectSyncService = class {
|
|
|
399
1480
|
constructor(options) {
|
|
400
1481
|
this.projectRoot = options.projectRoot;
|
|
401
1482
|
this.provider = options.provider;
|
|
402
|
-
this.primeUiRoot =
|
|
403
|
-
this.tempRoot =
|
|
404
|
-
this.exportsRoot =
|
|
1483
|
+
this.primeUiRoot = path5.join(this.projectRoot, ".primeui");
|
|
1484
|
+
this.tempRoot = path5.join(this.primeUiRoot, "temp");
|
|
1485
|
+
this.exportsRoot = path5.join(this.tempRoot, "exports");
|
|
405
1486
|
}
|
|
406
1487
|
async getProjectInfo() {
|
|
407
1488
|
await this.ensureTempLayout();
|
|
@@ -413,7 +1494,23 @@ var ProjectSyncService = class {
|
|
|
413
1494
|
}
|
|
414
1495
|
async createExport() {
|
|
415
1496
|
await this.ensureTempLayout();
|
|
416
|
-
|
|
1497
|
+
const result = await this.provider.createExport();
|
|
1498
|
+
const pagesSnapshotPath = buildPagesSnapshotPath(
|
|
1499
|
+
this.exportsRoot,
|
|
1500
|
+
result.export.id
|
|
1501
|
+
);
|
|
1502
|
+
const snapshot = {
|
|
1503
|
+
schemaVersion: 1,
|
|
1504
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1505
|
+
exportId: result.export.id,
|
|
1506
|
+
pages: result.pages
|
|
1507
|
+
};
|
|
1508
|
+
await writeUtf8(
|
|
1509
|
+
pagesSnapshotPath,
|
|
1510
|
+
`${JSON.stringify(snapshot, null, 2)}
|
|
1511
|
+
`
|
|
1512
|
+
);
|
|
1513
|
+
return result;
|
|
417
1514
|
}
|
|
418
1515
|
async downloadExportById(id) {
|
|
419
1516
|
await this.ensureTempLayout();
|
|
@@ -422,76 +1519,114 @@ var ProjectSyncService = class {
|
|
|
422
1519
|
if (!selected) {
|
|
423
1520
|
throw new Error(`Export not found: ${id}`);
|
|
424
1521
|
}
|
|
425
|
-
const targetZipPath =
|
|
426
|
-
const targetProjectPath =
|
|
1522
|
+
const targetZipPath = path5.join(this.exportsRoot, `${id}.zip`);
|
|
1523
|
+
const targetProjectPath = path5.join(this.exportsRoot, id);
|
|
427
1524
|
await ensureDir(this.exportsRoot);
|
|
428
1525
|
await this.provider.downloadExportArchive(id, targetZipPath);
|
|
429
1526
|
await resetDir(targetProjectPath);
|
|
430
1527
|
await extractZip(targetZipPath, targetProjectPath);
|
|
431
|
-
const
|
|
1528
|
+
const pagesSnapshotPath = buildPagesSnapshotPath(this.exportsRoot, id);
|
|
1529
|
+
let pagesSnapshot;
|
|
1530
|
+
try {
|
|
1531
|
+
const snapshotRaw = JSON.parse(
|
|
1532
|
+
await readFile4(pagesSnapshotPath, "utf-8")
|
|
1533
|
+
);
|
|
1534
|
+
pagesSnapshot = parsePagesSnapshot(snapshotRaw);
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1537
|
+
throw new Error(
|
|
1538
|
+
`Export pages snapshot is required for download id "${id}". Run primeui_create_export for this export before downloading. Details: ${reason}`
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
if (pagesSnapshot.exportId !== id) {
|
|
1542
|
+
throw new Error(
|
|
1543
|
+
`Export pages snapshot mismatch for id "${id}". Run primeui_create_export and retry download.`
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
const pages = pagesSnapshot.pages;
|
|
1547
|
+
const manifestPath = path5.join(this.exportsRoot, `${id}.manifest.json`);
|
|
1548
|
+
const manifest = {
|
|
1549
|
+
schemaVersion: 1,
|
|
1550
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1551
|
+
exportId: id,
|
|
1552
|
+
projectPath: targetProjectPath,
|
|
1553
|
+
pages
|
|
1554
|
+
};
|
|
1555
|
+
await writeUtf8(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
1556
|
+
`);
|
|
432
1557
|
return {
|
|
433
1558
|
exportId: id,
|
|
434
1559
|
projectPath: targetProjectPath,
|
|
1560
|
+
manifestPath,
|
|
435
1561
|
pages
|
|
436
1562
|
};
|
|
437
1563
|
}
|
|
1564
|
+
async copyPage(originPageSlug, actualPageSlug) {
|
|
1565
|
+
await this.ensureTempLayout();
|
|
1566
|
+
return copyPageFromExport({
|
|
1567
|
+
projectRoot: this.projectRoot,
|
|
1568
|
+
exportsRoot: this.exportsRoot,
|
|
1569
|
+
originPageSlug,
|
|
1570
|
+
actualPageSlug
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
438
1573
|
async clearTemp() {
|
|
439
1574
|
await resetDir(this.tempRoot);
|
|
440
|
-
await writeUtf8(
|
|
1575
|
+
await writeUtf8(path5.join(this.tempRoot, ".gitkeep"), "");
|
|
441
1576
|
await ensureDir(this.exportsRoot);
|
|
442
1577
|
}
|
|
443
1578
|
async ensureTempLayout() {
|
|
444
1579
|
await ensureDir(this.primeUiRoot);
|
|
445
1580
|
await ensureDir(this.tempRoot);
|
|
446
1581
|
await ensureDir(this.exportsRoot);
|
|
447
|
-
await writeUtf8(
|
|
1582
|
+
await writeUtf8(path5.join(this.tempRoot, ".gitkeep"), "");
|
|
448
1583
|
}
|
|
449
1584
|
};
|
|
450
1585
|
|
|
451
1586
|
// src/sources/api-provider.ts
|
|
452
1587
|
import { createWriteStream } from "fs";
|
|
453
1588
|
import { unlink } from "fs/promises";
|
|
454
|
-
import
|
|
1589
|
+
import path6 from "path";
|
|
455
1590
|
import { Readable, Transform } from "stream";
|
|
456
1591
|
import { pipeline } from "stream/promises";
|
|
457
|
-
import { z as
|
|
1592
|
+
import { z as z3 } from "zod";
|
|
458
1593
|
var DEFAULT_API_BASE_URL = "https://app.primeui.com/";
|
|
459
1594
|
var ZIP_CONTENT_TYPES = [
|
|
460
1595
|
"application/zip",
|
|
461
1596
|
"application/octet-stream",
|
|
462
1597
|
"application/x-zip-compressed"
|
|
463
1598
|
];
|
|
464
|
-
var exportStatusSchema2 =
|
|
465
|
-
var projectPageSchema =
|
|
466
|
-
id:
|
|
467
|
-
title:
|
|
468
|
-
slug:
|
|
469
|
-
pageType:
|
|
470
|
-
isReadyToExport:
|
|
471
|
-
pagePath:
|
|
472
|
-
componentsPath:
|
|
1599
|
+
var exportStatusSchema2 = z3.enum(["in_progress", "completed", "failed"]);
|
|
1600
|
+
var projectPageSchema = z3.object({
|
|
1601
|
+
id: z3.string(),
|
|
1602
|
+
title: z3.string(),
|
|
1603
|
+
slug: z3.string(),
|
|
1604
|
+
pageType: z3.string(),
|
|
1605
|
+
isReadyToExport: z3.boolean(),
|
|
1606
|
+
pagePath: z3.string(),
|
|
1607
|
+
componentsPath: z3.string()
|
|
473
1608
|
});
|
|
474
|
-
var projectInfoSchema =
|
|
475
|
-
projectId:
|
|
476
|
-
projectName:
|
|
477
|
-
metadata:
|
|
478
|
-
pages:
|
|
1609
|
+
var projectInfoSchema = z3.object({
|
|
1610
|
+
projectId: z3.string(),
|
|
1611
|
+
projectName: z3.string(),
|
|
1612
|
+
metadata: z3.record(z3.unknown()),
|
|
1613
|
+
pages: z3.array(projectPageSchema)
|
|
479
1614
|
});
|
|
480
|
-
var exportsResponseSchema =
|
|
481
|
-
exports:
|
|
482
|
-
|
|
483
|
-
id:
|
|
1615
|
+
var exportsResponseSchema = z3.object({
|
|
1616
|
+
exports: z3.array(
|
|
1617
|
+
z3.object({
|
|
1618
|
+
id: z3.string(),
|
|
484
1619
|
status: exportStatusSchema2,
|
|
485
|
-
createdAt:
|
|
1620
|
+
createdAt: z3.string().datetime({ offset: true })
|
|
486
1621
|
})
|
|
487
1622
|
)
|
|
488
1623
|
});
|
|
489
|
-
var createExportResponseSchema =
|
|
490
|
-
export:
|
|
491
|
-
id:
|
|
1624
|
+
var createExportResponseSchema = z3.object({
|
|
1625
|
+
export: z3.object({
|
|
1626
|
+
id: z3.string(),
|
|
492
1627
|
status: exportStatusSchema2
|
|
493
1628
|
}),
|
|
494
|
-
pages:
|
|
1629
|
+
pages: z3.array(projectPageSchema)
|
|
495
1630
|
});
|
|
496
1631
|
var PrimeUiApiContractError = class extends Error {
|
|
497
1632
|
constructor(endpoint, details) {
|
|
@@ -620,7 +1755,7 @@ var ApiProjectDataProvider = class {
|
|
|
620
1755
|
if (!response.body) {
|
|
621
1756
|
throw new PrimeUiApiContractError(endpoint, "response body is empty");
|
|
622
1757
|
}
|
|
623
|
-
await ensureDir(
|
|
1758
|
+
await ensureDir(path6.dirname(destinationPath));
|
|
624
1759
|
const zipStream = Readable.fromWeb(
|
|
625
1760
|
response.body
|
|
626
1761
|
);
|
|
@@ -708,22 +1843,30 @@ var ApiProjectDataProvider = class {
|
|
|
708
1843
|
|
|
709
1844
|
// src/service.ts
|
|
710
1845
|
async function main() {
|
|
711
|
-
const
|
|
712
|
-
|
|
1846
|
+
const resolvedProjectConfig = await resolvePrimeUiProjectConfig({
|
|
1847
|
+
cwd: process.cwd(),
|
|
1848
|
+
projectRootFromEnv: process.env.PRIMEUI_PROJECT_ROOT
|
|
1849
|
+
});
|
|
1850
|
+
const projectRoot = resolvedProjectConfig.projectRoot;
|
|
1851
|
+
const targetProjectRoot = path7.resolve(
|
|
1852
|
+
projectRoot,
|
|
1853
|
+
resolvedProjectConfig.projectConfig.targetProjectPath
|
|
1854
|
+
);
|
|
1855
|
+
const apiKey = await resolvePrimeUiApiKey({
|
|
1856
|
+
projectConfig: resolvedProjectConfig.projectConfig,
|
|
1857
|
+
apiKeyFromEnv: process.env.PRIMEUI_API_KEY
|
|
1858
|
+
});
|
|
713
1859
|
const provider = new ApiProjectDataProvider({
|
|
714
1860
|
apiKey,
|
|
715
1861
|
baseUrl: process.env.PRIMEUI_API_BASE_URL
|
|
716
1862
|
});
|
|
717
|
-
if (!apiKey) {
|
|
718
|
-
console.error(
|
|
719
|
-
"[primeui-mcp] warning: PRIMEUI_API_KEY is not set; API-backed tools will fail until it is configured."
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
1863
|
const syncService = new ProjectSyncService({ projectRoot, provider });
|
|
723
1864
|
const server = createPrimeUiMcpServer(syncService);
|
|
724
1865
|
const transport = new StdioServerTransport();
|
|
725
1866
|
await server.connect(transport);
|
|
726
|
-
console.error(
|
|
1867
|
+
console.error(
|
|
1868
|
+
`[primeui-mcp] running for root: ${projectRoot}; target project: ${targetProjectRoot}`
|
|
1869
|
+
);
|
|
727
1870
|
}
|
|
728
1871
|
main().catch((error) => {
|
|
729
1872
|
console.error("[primeui-mcp] failed to start", error);
|