@codepress/codepress-engine 0.4.0-dev.tsc.20251014034858

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/server.js ADDED
@@ -0,0 +1,1076 @@
1
+ "use strict";
2
+ // Codepress Dev Server
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.createApp = createApp;
41
+ exports.getProjectStructure = getProjectStructure;
42
+ exports.startServer = startServer;
43
+ const cors_1 = __importDefault(require("@fastify/cors"));
44
+ const fastify_1 = __importDefault(require("fastify"));
45
+ const fs = __importStar(require("node:fs"));
46
+ const os = __importStar(require("node:os"));
47
+ const path = __importStar(require("node:path"));
48
+ const prettier = __importStar(require("prettier"));
49
+ const index_1 = require("./index");
50
+ /**
51
+ * Normalizes a possibly-relative or malformed absolute path into an absolute path.
52
+ * - Uses CWD for relative paths
53
+ * - Fixes common case where macOS absolute paths lose their leading slash (e.g., "Users/...")
54
+ * @param {string} inputPath
55
+ * @returns {string}
56
+ */
57
+ function toAbsolutePath(inputPath) {
58
+ if (!inputPath)
59
+ return process.cwd();
60
+ const trimmedPath = String(inputPath).trim();
61
+ // Fix macOS-like absolute paths missing the leading slash, e.g. "Users/..."
62
+ const looksLikePosixAbsNoSlash = process.platform !== "win32" &&
63
+ (trimmedPath.startsWith("Users" + path.sep) ||
64
+ trimmedPath.startsWith("Volumes" + path.sep));
65
+ const candidate = looksLikePosixAbsNoSlash
66
+ ? path.sep + trimmedPath
67
+ : trimmedPath;
68
+ return path.isAbsolute(candidate)
69
+ ? candidate
70
+ : path.join(process.cwd(), candidate);
71
+ }
72
+ /**
73
+ * Gets the port to use for the server
74
+ * @returns {number} The configured port
75
+ */
76
+ function getServerPort() {
77
+ // Use environment variable or default to 4321
78
+ return parseInt(process.env.CODEPRESS_DEV_PORT || "4321", 10);
79
+ }
80
+ /**
81
+ * Create a lock file to ensure only one instance runs
82
+ * @returns {boolean} True if lock was acquired, false otherwise
83
+ */
84
+ function acquireLock() {
85
+ try {
86
+ const lockPath = path.join(os.tmpdir(), "codepress-dev-server.lock");
87
+ // Try to read the lock file to check if the server is already running
88
+ const lockData = fs.existsSync(lockPath)
89
+ ? JSON.parse(fs.readFileSync(lockPath, "utf8"))
90
+ : null;
91
+ if (lockData) {
92
+ // Check if the process in the lock file is still running
93
+ try {
94
+ // On Unix-like systems, sending signal 0 checks if process exists
95
+ process.kill(lockData.pid, 0);
96
+ // Process exists, lock is valid
97
+ return false;
98
+ }
99
+ catch (err) {
100
+ console.error("Error checking lock process:", err);
101
+ // Process doesn't exist, lock is stale
102
+ // Continue to create a new lock
103
+ }
104
+ }
105
+ // Create a new lock file
106
+ fs.writeFileSync(lockPath, JSON.stringify({
107
+ pid: process.pid,
108
+ timestamp: Date.now(),
109
+ }));
110
+ return true;
111
+ }
112
+ catch (err) {
113
+ console.error("Error acquiring lock:", err);
114
+ // If anything fails, assume we couldn't get the lock
115
+ return false;
116
+ }
117
+ }
118
+ // Track server instance (singleton pattern)
119
+ let serverInstance = null;
120
+ /**
121
+ * Make an HTTP request to the FastAPI backend using fetch
122
+ * @param {string} method The HTTP method
123
+ * @param {string} endpoint The API endpoint
124
+ * @param {Object} data The request payload
125
+ * @param {string} incomingAuthHeader The incoming Authorization header
126
+ * @returns {Promise<Object>} The response data
127
+ */
128
+ /**
129
+ * Call backend API with streaming support
130
+ * @param {string} method HTTP method
131
+ * @param {string} endpoint API endpoint
132
+ * @param {Object} data Request data
133
+ * @param {string} incomingAuthHeader Authorization header
134
+ * @param {Function} onStreamEvent Callback for stream events
135
+ * @returns {Promise<Object>} Final API response
136
+ */
137
+ async function callBackendApiStreaming(method, endpoint, data, incomingAuthHeader, onStreamEvent) {
138
+ var _a;
139
+ // Backend API settings
140
+ const apiHost = process.env.CODEPRESS_BACKEND_HOST || "localhost";
141
+ const apiPort = parseInt(process.env.CODEPRESS_BACKEND_PORT || "8007", 10);
142
+ const apiPath = endpoint.startsWith("/")
143
+ ? endpoint.replace("/", "")
144
+ : endpoint;
145
+ const protocol = apiHost === "localhost" || apiHost === "127.0.0.1" ? "http" : "https";
146
+ const url = `${protocol}://${apiHost}:${apiPort}/${apiPath}`;
147
+ const requestOptions = {
148
+ method,
149
+ headers: {
150
+ "Content-Type": "application/json",
151
+ Accept: "text/event-stream",
152
+ },
153
+ };
154
+ if (incomingAuthHeader) {
155
+ requestOptions.headers.Authorization = incomingAuthHeader;
156
+ }
157
+ if (data) {
158
+ requestOptions.body = JSON.stringify(data);
159
+ }
160
+ try {
161
+ console.log(`\x1b[36mℹ Calling backend streaming API: ${method} ${url}\x1b[0m`);
162
+ const response = await fetch(url, requestOptions);
163
+ if (!response.ok) {
164
+ const errorText = await response.text();
165
+ throw new Error(`Backend API error (${response.status}): ${errorText}`);
166
+ }
167
+ // Handle streaming response
168
+ if (((_a = response.headers.get("content-type")) === null || _a === void 0 ? void 0 : _a.includes("text/event-stream")) &&
169
+ onStreamEvent) {
170
+ const body = response.body;
171
+ if (!body) {
172
+ throw new Error("Backend streaming response has no body");
173
+ }
174
+ const reader = body.getReader();
175
+ const decoder = new TextDecoder();
176
+ let finalResult = null;
177
+ try {
178
+ while (true) {
179
+ const { done, value } = await reader.read();
180
+ if (done)
181
+ break;
182
+ const chunk = decoder.decode(value, { stream: true });
183
+ const lines = chunk.split("\n");
184
+ for (const line of lines) {
185
+ if (line.startsWith("data: ")) {
186
+ try {
187
+ const eventData = JSON.parse(line.slice(6));
188
+ // Forward the event to the client
189
+ if (onStreamEvent) {
190
+ onStreamEvent(eventData);
191
+ }
192
+ // Capture final result if this is a completion event
193
+ if (eventData.type === "final_result") {
194
+ finalResult = eventData.result;
195
+ }
196
+ else if (eventData.type === "complete") {
197
+ // Use the last result we captured
198
+ break;
199
+ }
200
+ }
201
+ catch (parseError) {
202
+ console.error("Error parsing SSE data:", parseError, "Line:", line);
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+ finally {
209
+ reader.releaseLock();
210
+ }
211
+ // Return the agent's final result payload; may be undefined if no final_result arrived
212
+ return finalResult;
213
+ }
214
+ else {
215
+ // Fallback to regular JSON response
216
+ return (await response.json());
217
+ }
218
+ }
219
+ catch (error) {
220
+ console.error(`\x1b[31m✗ Backend streaming API call failed: ${error.message}\x1b[0m`);
221
+ throw error;
222
+ }
223
+ }
224
+ async function callBackendApi(method, endpoint, data, incomingAuthHeader) {
225
+ // Backend API settings
226
+ const apiHost = process.env.CODEPRESS_BACKEND_HOST || "localhost";
227
+ const apiPort = parseInt(process.env.CODEPRESS_BACKEND_PORT || "8007", 10);
228
+ const apiPath = endpoint.startsWith("/")
229
+ ? endpoint.replace("/", "")
230
+ : endpoint;
231
+ // Build the complete URL - detect if using localhost for HTTP, otherwise use HTTPS
232
+ const protocol = apiHost === "localhost" || apiHost === "127.0.0.1" ? "http" : "https";
233
+ console.log(`\x1b[36mℹ API Path: ${apiPath} \x1b[0m`);
234
+ const url = `${protocol}://${apiHost}${apiPort ? `:${apiPort}` : ""}/v1/${apiPath}`;
235
+ console.log(`\x1b[36mℹ Sending request to ${url} \x1b[0m`);
236
+ try {
237
+ // First try to use API token from environment variable
238
+ let authToken = process.env.CODEPRESS_API_TOKEN;
239
+ // Debug: Log environment token status
240
+ console.log(`\x1b[36mℹ Environment API token: ${authToken ? "[PRESENT]" : "[NOT SET]"}\x1b[0m`);
241
+ console.log(`\x1b[36mℹ Incoming auth header: ${incomingAuthHeader ? "[PRESENT]" : "[NOT PROVIDED]"}\x1b[0m`);
242
+ // If no API token, try to use the incoming Authorization header
243
+ if (!authToken && incomingAuthHeader) {
244
+ authToken = incomingAuthHeader.split(" ")[1]; // Extract token part
245
+ console.log(`\x1b[36mℹ Using incoming Authorization header for authentication\x1b[0m`);
246
+ }
247
+ // Prepare headers with authentication if token exists
248
+ const headers = {
249
+ "Content-Type": "application/json",
250
+ };
251
+ if (authToken) {
252
+ headers["Authorization"] = `Bearer ${authToken}`;
253
+ // Log which auth method we're using (but don't expose the actual token)
254
+ console.log(`\x1b[36mℹ Using ${process.env.CODEPRESS_API_TOKEN ? "API Token" : "GitHub OAuth Token"} for authentication\x1b[0m`);
255
+ console.log(`\x1b[36mℹ Final auth header: Bearer ${authToken.substring(0, 10)}...\x1b[0m`);
256
+ }
257
+ else {
258
+ console.log("\x1b[33m⚠ No authentication token available\x1b[0m");
259
+ }
260
+ const response = await fetch(url, {
261
+ method,
262
+ headers,
263
+ body: data ? JSON.stringify(data) : undefined,
264
+ });
265
+ // Get the response text
266
+ const responseText = await response.text();
267
+ // Debug: Log response status and preview
268
+ console.log(`\x1b[36mℹ Response status: ${response.status}\x1b[0m`);
269
+ console.log(`\x1b[36mℹ Response preview: ${responseText.substring(0, 100)}...\x1b[0m`);
270
+ // Check if response is successful
271
+ if (!response.ok) {
272
+ throw new Error(`API request failed with status ${response.status}: ${responseText}`);
273
+ }
274
+ // Try to parse the response as JSON
275
+ try {
276
+ return JSON.parse(responseText);
277
+ }
278
+ catch (err) {
279
+ throw new Error(`Invalid JSON response: ${err.message}`);
280
+ }
281
+ }
282
+ catch (err) {
283
+ // Handle network errors and other issues
284
+ if (err.name === "FetchError") {
285
+ throw new Error(`Network error: ${err.message}`);
286
+ }
287
+ // Re-throw the original error
288
+ throw err;
289
+ }
290
+ }
291
+ /**
292
+ * Service: Save image data to file system
293
+ * @param {Object} params - Function parameters
294
+ * @param {string} params.imageData - Base64 image data
295
+ * @param {string} params.filename - Optional filename
296
+ * @returns {Promise<string|null>} The saved image path or null if failed
297
+ */
298
+ async function saveImageData({ imageData, filename, }) {
299
+ if (!imageData)
300
+ return null;
301
+ try {
302
+ const imageDir = path.join(process.cwd(), "public");
303
+ if (!fs.existsSync(imageDir)) {
304
+ fs.mkdirSync(imageDir, { recursive: true });
305
+ console.log(`\x1b[36mℹ Created directory: ${imageDir}\x1b[0m`);
306
+ }
307
+ let imagePath;
308
+ let base64Data;
309
+ if (filename) {
310
+ imagePath = path.join(imageDir, filename);
311
+ // When filename is provided, assume image_data is just the base64 string
312
+ const match = imageData.match(/^data:image\/[\w+]+\;base64,(.+)$/);
313
+ if (match === null || match === void 0 ? void 0 : match[1]) {
314
+ base64Data = match[1]; // Extract if full data URI is sent
315
+ }
316
+ else {
317
+ base64Data = imageData; // Assume raw base64
318
+ }
319
+ console.log(`\x1b[36mℹ Using provided filename: ${filename}\x1b[0m`);
320
+ }
321
+ else {
322
+ // Fallback to existing logic if filename is not provided
323
+ const match = imageData.match(/^data:image\/([\w+]+);base64,(.+)$/);
324
+ let imageExtension;
325
+ if ((match === null || match === void 0 ? void 0 : match[1]) && (match === null || match === void 0 ? void 0 : match[2])) {
326
+ imageExtension = match[1];
327
+ base64Data = match[2];
328
+ }
329
+ else {
330
+ base64Data = imageData;
331
+ imageExtension = "png";
332
+ console.log("\x1b[33m⚠ Image data URI prefix not found and no filename provided, defaulting to .png extension.\x1b[0m");
333
+ }
334
+ if (imageExtension === "jpeg")
335
+ imageExtension = "jpg";
336
+ if (imageExtension === "svg+xml")
337
+ imageExtension = "svg";
338
+ const imageName = `image_${Date.now()}.${imageExtension}`;
339
+ imagePath = path.join(imageDir, imageName);
340
+ }
341
+ const imageBuffer = Buffer.from(base64Data, "base64");
342
+ fs.writeFileSync(imagePath, imageBuffer);
343
+ console.log(`\x1b[32m✓ Image saved to ${imagePath}\x1b[0m`);
344
+ return imagePath;
345
+ }
346
+ catch (imgError) {
347
+ console.error(`\x1b[31m✗ Error saving image: ${imgError.message}\x1b[0m`);
348
+ return null;
349
+ }
350
+ }
351
+ /**
352
+ * Service: Read file content from encoded location
353
+ * @param {string} encodedLocation The encoded file location
354
+ * @returns {Object} File data with path and content
355
+ */
356
+ function readFileFromEncodedLocation(encodedLocation) {
357
+ const encodedFilePath = encodedLocation.split(":")[0];
358
+ const filePath = (0, index_1.decode)(encodedFilePath);
359
+ console.log(`\x1b[36mℹ Decoded file path: ${filePath}\x1b[0m`);
360
+ // If filePath is absolute, use it directly. Otherwise, join with cwd.
361
+ const targetFile = path.isAbsolute(filePath)
362
+ ? filePath
363
+ : path.join(process.cwd(), filePath);
364
+ console.log(`\x1b[36mℹ Reading file: ${targetFile}\x1b[0m`);
365
+ const fileContent = fs.readFileSync(targetFile, "utf8");
366
+ return { filePath, targetFile, fileContent };
367
+ }
368
+ /**
369
+ * Service: Get changes from backend (original endpoint)
370
+ * @param {Object} params Request parameters
371
+ * @param {string} params.githubRepoName The GitHub repository name
372
+ * @param {Array<Object>} params.fileChanges Array of file change objects to process. Each object represents changes for a single file.
373
+ * @param {string} params.fileChanges[].encoded_location The encoded file location identifier used to determine which file to modify
374
+ * @param {string} params.fileChanges[].file_content The current content of the file being modified
375
+ * @param {Array<Object>} params.fileChanges[].style_changes Array of style-related changes to apply to the file. Each object contains styling modifications.
376
+ * @param {Array<Object>} params.fileChanges[].text_changes Array of text-based changes to apply to the file. Each object contains:
377
+ * @param {string} params.fileChanges[].text_changes[].old_text The original HTML/text content to be replaced (mapped from old_html)
378
+ * @param {string} params.fileChanges[].text_changes[].new_text The new HTML/text content to replace with (mapped from new_html)
379
+ * @param {string} [params.fileChanges[].text_changes[].encoded_location] The encoded location for this specific text change (inherited from parent change object)
380
+ * @param {Array<Object>} [params.fileChanges[].text_changes[].style_changes] Any style changes associated with this text change (inherited from parent change object)
381
+ * @param {string} [params.authHeader] The authorization header for backend API authentication (Bearer token)
382
+ * @returns {Promise<Object>} Backend response containing the processed changes, typically with an 'updated_files' property mapping file paths to their new content
383
+ */
384
+ async function getChanges({ githubRepoName, fileChanges, authHeader, }) {
385
+ console.log(`\x1b[36mℹ Getting changes from backend for ${fileChanges.length} files\x1b[0m`);
386
+ return await callBackendApi("POST", "code-sync/get-changes", {
387
+ github_repo_name: githubRepoName,
388
+ file_changes: fileChanges,
389
+ }, authHeader);
390
+ }
391
+ /**
392
+ * Service: Apply full file replacement and format code
393
+ * @param {string} modifiedContent The complete new file content
394
+ * @param {string} targetFile Target file path
395
+ * @returns {Promise<string>} Formatted code
396
+ */
397
+ function pickPrettierParser(filePath) {
398
+ const lower = (filePath || "").toLowerCase();
399
+ if (lower.endsWith(".ts") || lower.endsWith(".tsx"))
400
+ return "typescript";
401
+ return "babel"; // default for .js/.jsx and others
402
+ }
403
+ async function tryFormatWithPrettierOrNull(code, filePath) {
404
+ try {
405
+ const result = await Promise.resolve(prettier.format(code, {
406
+ parser: pickPrettierParser(filePath),
407
+ semi: true,
408
+ singleQuote: false,
409
+ }));
410
+ return result;
411
+ }
412
+ catch (err) {
413
+ console.error("Error formatting code with Prettier:", err);
414
+ return null;
415
+ }
416
+ }
417
+ function closeUnclosedJsxTags(code) {
418
+ try {
419
+ const tagRegex = /<\/?([A-Za-z][A-Za-z0-9]*)\b[^>]*?\/?>(?!\s*<\!)/g;
420
+ const selfClosingRegex = /<([A-Za-z][A-Za-z0-9]*)\b[^>]*?\/>/;
421
+ const stack = [];
422
+ while (true) {
423
+ const nextMatch = tagRegex.exec(code);
424
+ if (nextMatch === null)
425
+ break;
426
+ const full = nextMatch[0];
427
+ const name = nextMatch[1];
428
+ const isClose = full.startsWith("</");
429
+ const isSelfClosing = selfClosingRegex.test(full);
430
+ if (isSelfClosing)
431
+ continue;
432
+ if (!isClose) {
433
+ stack.push(name);
434
+ }
435
+ else {
436
+ if (stack.length && stack[stack.length - 1] === name) {
437
+ stack.pop();
438
+ }
439
+ else {
440
+ const idx = stack.lastIndexOf(name);
441
+ if (idx !== -1)
442
+ stack.splice(idx, 1);
443
+ }
444
+ }
445
+ }
446
+ if (stack.length === 0)
447
+ return code;
448
+ let suffix = "";
449
+ for (let i = stack.length - 1; i >= 0; i--) {
450
+ const tag = stack[i];
451
+ suffix += `</${tag}>`;
452
+ }
453
+ return code + "\n" + suffix + "\n";
454
+ }
455
+ catch {
456
+ return code;
457
+ }
458
+ }
459
+ async function applyFullFileReplacement(modifiedContent, targetFile) {
460
+ console.log(`\x1b[36mℹ Applying full file replacement\x1b[0m`);
461
+ // Ensure folder
462
+ try {
463
+ const dir = path.dirname(targetFile);
464
+ if (!fs.existsSync(dir)) {
465
+ fs.mkdirSync(dir, { recursive: true });
466
+ console.log(`\x1b[36mℹ Created directory: ${dir}\x1b[0m`);
467
+ }
468
+ }
469
+ catch (mkdirErr) {
470
+ console.error(`\x1b[31m✗ Failed to ensure directory for ${targetFile}: ${mkdirErr.message}\x1b[0m`);
471
+ }
472
+ let formattedCode = "";
473
+ if (typeof modifiedContent === "string") {
474
+ // Format with Prettier
475
+ try {
476
+ formattedCode = await prettier.format(modifiedContent, {
477
+ parser: pickPrettierParser(targetFile),
478
+ semi: true,
479
+ singleQuote: false,
480
+ });
481
+ }
482
+ catch (prettierError) {
483
+ console.error("Prettier formatting failed:", prettierError);
484
+ // If formatting fails, use the unformatted code
485
+ formattedCode = modifiedContent;
486
+ }
487
+ fs.writeFileSync(targetFile, formattedCode, "utf8");
488
+ }
489
+ else if (modifiedContent.type === "binary" && modifiedContent.base64) {
490
+ const buffer = Buffer.from(modifiedContent.base64, "base64");
491
+ formattedCode = "binary_encoded_file";
492
+ fs.writeFileSync(targetFile, buffer);
493
+ }
494
+ else {
495
+ console.warn(`Unknown file type for ${targetFile}, skipping`);
496
+ formattedCode = "";
497
+ }
498
+ console.log(`\x1b[32m✓ Updated file ${targetFile} with complete file replacement\x1b[0m`);
499
+ return formattedCode;
500
+ }
501
+ /**
502
+ * Handle streaming agent requests with Server-Sent Events
503
+ * @param {Object} params - Function parameters
504
+ * @param {Object} params.request - Fastify request object
505
+ * @param {Object} params.reply - Fastify reply object
506
+ * @param {Object} params.data - Request body data
507
+ * @param {string} params.authHeader - Authorization header
508
+ * @param {string} params.fileContent - The file content to process
509
+ */
510
+ async function handleStreamingAgentRequest({ reply, data, authHeader, fileContent, }) {
511
+ const { encoded_location, github_repo_name, user_instruction, branch_name } = data;
512
+ // Set up Server-Sent Events headers
513
+ reply.raw.writeHead(200, {
514
+ "Content-Type": "text/event-stream",
515
+ "Cache-Control": "no-cache",
516
+ Connection: "keep-alive",
517
+ "Access-Control-Allow-Origin": "*",
518
+ "Access-Control-Allow-Headers": "Cache-Control",
519
+ });
520
+ // Apply incremental updates to disk for hot reload
521
+ async function writeIncrementalUpdate(eventData) {
522
+ try {
523
+ if (eventData &&
524
+ eventData.type === "file_update" &&
525
+ eventData.file_path &&
526
+ typeof eventData.content === "string") {
527
+ // Normalize .tmp paths emitted by editors before writing
528
+ let normalizedPath = eventData.file_path;
529
+ if (normalizedPath.includes(".tmp.")) {
530
+ const tmpIdx = normalizedPath.indexOf(".tmp.");
531
+ normalizedPath = normalizedPath.slice(0, tmpIdx);
532
+ }
533
+ const targetFilePath = toAbsolutePath(normalizedPath);
534
+ const candidate = eventData.content;
535
+ let formatted = await tryFormatWithPrettierOrNull(candidate, targetFilePath);
536
+ if (!formatted) {
537
+ const closed = closeUnclosedJsxTags(candidate);
538
+ formatted = await tryFormatWithPrettierOrNull(closed, targetFilePath);
539
+ }
540
+ if (formatted) {
541
+ await applyFullFileReplacement(formatted, targetFilePath);
542
+ }
543
+ else {
544
+ console.warn("Skipping incremental write due to unparseable content for", targetFilePath);
545
+ }
546
+ }
547
+ if (eventData &&
548
+ eventData.type === "final_result" &&
549
+ eventData.result &&
550
+ eventData.result.updated_files) {
551
+ const updatedEntries = Object.entries(eventData.result.updated_files);
552
+ for (const [filePath, newContent] of updatedEntries) {
553
+ let p = filePath;
554
+ if (p.includes(".tmp.")) {
555
+ p = p.slice(0, p.indexOf(".tmp."));
556
+ }
557
+ const targetFilePath = toAbsolutePath(p);
558
+ let formatted = await tryFormatWithPrettierOrNull(newContent, targetFilePath);
559
+ if (!formatted) {
560
+ const closed = closeUnclosedJsxTags(newContent);
561
+ formatted = await tryFormatWithPrettierOrNull(closed, targetFilePath);
562
+ }
563
+ await applyFullFileReplacement(formatted !== null && formatted !== void 0 ? formatted : newContent, targetFilePath);
564
+ }
565
+ }
566
+ }
567
+ catch (e) {
568
+ console.error("Failed to apply incremental update:", e);
569
+ }
570
+ }
571
+ // Function to send SSE data
572
+ function sendEvent(eventData) {
573
+ const data = JSON.stringify(eventData);
574
+ reply.raw.write(`data: ${data}\n\n`);
575
+ }
576
+ try {
577
+ // Call the backend for agent changes with streaming
578
+ // The backend will handle all streaming events from the agent
579
+ const backendResponse = await callBackendApiStreaming("POST", "v1/code-sync/get-agent-changes", {
580
+ github_repo_name: github_repo_name,
581
+ encoded_location: encoded_location,
582
+ file_content: fileContent,
583
+ branch_name: branch_name,
584
+ user_instruction: user_instruction,
585
+ }, authHeader, async (evt) => {
586
+ await writeIncrementalUpdate(evt);
587
+ sendEvent(evt);
588
+ });
589
+ console.log(`\x1b[36mℹ backendResponse to agent: ${JSON.stringify(backendResponse)}\x1b[0m`);
590
+ // Handle the response and apply changes
591
+ if (backendResponse === null || backendResponse === void 0 ? void 0 : backendResponse.updated_files) {
592
+ const updatedFilePaths = [];
593
+ const updatedEntries = Object.entries(backendResponse.updated_files);
594
+ for (const [filePath, newContent] of updatedEntries) {
595
+ const targetFilePath = toAbsolutePath(filePath);
596
+ await applyFullFileReplacement(newContent, targetFilePath);
597
+ updatedFilePaths.push(filePath);
598
+ }
599
+ // Send final success event
600
+ sendEvent({
601
+ type: "final_result",
602
+ result: {
603
+ success: true,
604
+ updated_file_paths: updatedFilePaths,
605
+ },
606
+ success: true,
607
+ message: `✅ Changes applied successfully to ${updatedFilePaths.length} file(s)!`,
608
+ ephemeral: false,
609
+ });
610
+ }
611
+ else {
612
+ console.log(backendResponse);
613
+ throw new Error("No valid response from backend");
614
+ }
615
+ // Send completion event
616
+ sendEvent({ type: "complete" });
617
+ }
618
+ catch (error) {
619
+ console.error(`\x1b[31m✗ Error in streaming agent: ${error.message}\x1b[0m`);
620
+ sendEvent({
621
+ type: "error",
622
+ error: error.message,
623
+ ephemeral: false,
624
+ });
625
+ }
626
+ reply.raw.end();
627
+ }
628
+ /**
629
+ * Create and configure the Fastify app
630
+ * @returns {Object} The configured Fastify instance
631
+ */
632
+ function createApp() {
633
+ const app = (0, fastify_1.default)({
634
+ logger: false, // Disable built-in logging since we have custom logging
635
+ });
636
+ // Register CORS plugin
637
+ app.register(cors_1.default, {
638
+ origin: "*",
639
+ methods: ["GET", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"],
640
+ allowedHeaders: [
641
+ "X-Requested-With",
642
+ "content-type",
643
+ "Authorization",
644
+ "Cache-Control",
645
+ "Accept",
646
+ ],
647
+ credentials: true,
648
+ });
649
+ // Ping route
650
+ app.get("/ping", async (_request, reply) => {
651
+ return reply.code(200).type("text/plain").send("pong");
652
+ });
653
+ // Meta route
654
+ app.get("/meta", async (_request, reply) => {
655
+ // Try to get package version but don't fail if not available
656
+ let version = "0.0.0";
657
+ try {
658
+ // In production builds, use a relative path that works with the installed package structure
659
+ version = require("../package.json").version;
660
+ }
661
+ catch (err) {
662
+ console.error("Error getting package version:", err);
663
+ // Ignore error, use default version
664
+ }
665
+ return reply.code(200).send({
666
+ name: "Codepress Dev Server",
667
+ version: version,
668
+ environment: process.env.NODE_ENV || "development",
669
+ uptime: process.uptime(),
670
+ });
671
+ });
672
+ // Project structure route
673
+ app.get("/project-structure", async (_request, reply) => {
674
+ try {
675
+ const structure = getProjectStructure();
676
+ return reply.code(200).send({
677
+ success: true,
678
+ structure: structure,
679
+ });
680
+ }
681
+ catch (error) {
682
+ console.error("Error getting project structure:", error);
683
+ return reply.code(500).send({
684
+ success: false,
685
+ error: "Failed to get project structure",
686
+ message: error.message,
687
+ });
688
+ }
689
+ });
690
+ // Visual editor API route for regular agent changes
691
+ app.post("/visual-editor-api", async (request, reply) => {
692
+ try {
693
+ const { changes, github_repo_name } = request.body;
694
+ const authHeader = request.headers.authorization || request.headers["authorization"];
695
+ console.log(`\x1b[36mℹ Auth header received: ${authHeader ? "[PRESENT]" : "[MISSING]"}\x1b[0m`);
696
+ if (!Array.isArray(changes)) {
697
+ return reply.code(400).send({
698
+ error: "Invalid request format: 'changes' must be an array.",
699
+ });
700
+ }
701
+ console.log(`\x1b[36mℹ Visual Editor API Request: Received ${changes.length} changes for repo ${github_repo_name}\x1b[0m`);
702
+ const changesWithDimensions = changes.filter((change) => change.browser_width && change.browser_height);
703
+ if (changesWithDimensions.length > 0) {
704
+ const sampleChange = changesWithDimensions[0];
705
+ console.log(`\x1b[36mℹ Browser dimensions detected: ${sampleChange.browser_width}x${sampleChange.browser_height}\x1b[0m`);
706
+ }
707
+ else {
708
+ console.log(`\x1b[33m⚠ No browser dimensions found in changes\x1b[0m`);
709
+ }
710
+ const uniqueEncodedLocations = new Set();
711
+ const validChanges = [];
712
+ for (const change of changes) {
713
+ console.log(`\x1b[36mℹ change: ${JSON.stringify(change)}\x1b[0m`);
714
+ try {
715
+ if (!change.encoded_location) {
716
+ console.warn(`\x1b[33m⚠ Skipping change with missing encoded_location.\x1b[0m`);
717
+ continue;
718
+ }
719
+ const encodedFilePath = change.encoded_location.split(":")[0];
720
+ const targetFile = (0, index_1.decode)(encodedFilePath);
721
+ if (!targetFile) {
722
+ console.warn(`\x1b[33m⚠ Skipping change with undecodable file from encoded_location: ${change.encoded_location}.\x1b[0m`);
723
+ continue;
724
+ }
725
+ const hasStyleChanges = change.style_changes && change.style_changes.length > 0;
726
+ const hasTextChanges = change.text_changes && change.text_changes.length > 0;
727
+ const hasMoveChanges = change.move_changes && change.move_changes.length > 0;
728
+ if (!hasStyleChanges && !hasTextChanges && !hasMoveChanges) {
729
+ console.warn(`\x1b[33m⚠ Skipping change with no style, text, or move changes.\x1b[0m`);
730
+ continue;
731
+ }
732
+ uniqueEncodedLocations.add(change.encoded_location);
733
+ validChanges.push(change);
734
+ }
735
+ catch (err) {
736
+ console.error(`\x1b[31m✖ Error processing change for location: ${change.encoded_location}\x1b[0m`, err);
737
+ }
738
+ }
739
+ const fileContentMap = new Map();
740
+ uniqueEncodedLocations.forEach((encodedLocation) => {
741
+ try {
742
+ const { fileContent } = readFileFromEncodedLocation(encodedLocation);
743
+ fileContentMap.set(encodedLocation, fileContent);
744
+ }
745
+ catch (err) {
746
+ console.error(`\x1b[31m✖ Error reading file for location: ${encodedLocation}\x1b[0m`, err);
747
+ }
748
+ });
749
+ console.log(`\x1b[36mℹ Pre-fetched ${fileContentMap.size} unique files for ${validChanges.length} changes\x1b[0m`);
750
+ const fileChangesForBackend = [];
751
+ for (const change of validChanges) {
752
+ try {
753
+ const fileContent = fileContentMap.get(change.encoded_location);
754
+ if (!fileContent) {
755
+ console.warn(`\x1b[33m⚠ Skipping change with missing file content for: ${change.encoded_location}\x1b[0m`);
756
+ continue;
757
+ }
758
+ fileChangesForBackend.push({
759
+ encoded_location: change.encoded_location,
760
+ file_content: fileContent,
761
+ changes: [
762
+ {
763
+ style_changes: change.style_changes || [],
764
+ text_changes: change.text_changes || [],
765
+ move_changes: change.move_changes || [],
766
+ },
767
+ ],
768
+ browser_width: change.browser_width,
769
+ browser_height: change.browser_height,
770
+ });
771
+ }
772
+ catch (err) {
773
+ console.error(`\x1b[31m✖ Error processing change for location: ${change.encoded_location}\x1b[0m`, err);
774
+ }
775
+ }
776
+ for (const change of changes) {
777
+ if (change.image_data && change.filename) {
778
+ await saveImageData({
779
+ imageData: change.image_data,
780
+ filename: change.filename,
781
+ });
782
+ }
783
+ }
784
+ if (fileChangesForBackend.length === 0) {
785
+ return reply.code(200).send({
786
+ message: "No changes to apply.",
787
+ updatedFiles: [],
788
+ });
789
+ }
790
+ console.log(`\x1b[36mℹ Sending request for ${fileChangesForBackend.length} individual changes (${changes.length} total original changes)\x1b[0m`);
791
+ const backendChangesWithDimensions = fileChangesForBackend.filter((change) => change.browser_width && change.browser_height);
792
+ if (backendChangesWithDimensions.length > 0) {
793
+ console.log(`\x1b[36mℹ Sending browser dimensions to backend for ${backendChangesWithDimensions.length} changes\x1b[0m`);
794
+ }
795
+ const backendResponse = await getChanges({
796
+ githubRepoName: github_repo_name,
797
+ fileChanges: fileChangesForBackend,
798
+ authHeader,
799
+ });
800
+ const updatedFiles = new Set();
801
+ if (backendResponse === null || backendResponse === void 0 ? void 0 : backendResponse.updated_files) {
802
+ console.log(`\x1b[36mℹ Processing updated_files format\x1b[0m`);
803
+ const updatedEntries = Object.entries(backendResponse.updated_files);
804
+ for (const [filePath, newContent] of updatedEntries) {
805
+ const targetFile = toAbsolutePath(filePath);
806
+ await applyFullFileReplacement(newContent, targetFile);
807
+ updatedFiles.add(targetFile);
808
+ }
809
+ }
810
+ if (updatedFiles.size === 0) {
811
+ return reply.code(200).send({
812
+ message: "No changes were applied.",
813
+ updatedFiles: [],
814
+ });
815
+ }
816
+ return reply.code(200).send({
817
+ message: `Changes applied successfully to ${updatedFiles.size} file(s). Processed ${changes.length} individual changes with preserved line number information.`,
818
+ updatedFiles: Array.from(updatedFiles),
819
+ });
820
+ }
821
+ catch (err) {
822
+ console.error(`\x1b[31m✖ Fatal error in /visual-editor-api: ${err.message}\x1b[0m`);
823
+ return reply.code(500).send({
824
+ error: "An internal server error occurred",
825
+ details: err.message,
826
+ });
827
+ }
828
+ });
829
+ // Visual editor API route for agent changes
830
+ app.post("/visual-editor-api-agent", async (request, reply) => {
831
+ try {
832
+ const data = request.body;
833
+ const { encoded_location, image_data, filename } = data;
834
+ const authHeader = request.headers.authorization || request.headers["authorization"];
835
+ console.log(`\x1b[36mℹ [visual-editor-api-agent] Auth header received: ${authHeader ? "[PRESENT]" : "[MISSING]"}, Always streaming\x1b[0m`);
836
+ if (!encoded_location) {
837
+ return reply.code(400).send({ error: "Missing encoded_location" });
838
+ }
839
+ const { fileContent } = readFileFromEncodedLocation(encoded_location);
840
+ // Save image data before processing
841
+ await saveImageData({ imageData: image_data, filename });
842
+ // Always use streaming for agent requests
843
+ return await handleStreamingAgentRequest({
844
+ reply,
845
+ data,
846
+ authHeader,
847
+ fileContent,
848
+ });
849
+ }
850
+ catch (err) {
851
+ console.error(`Error in /visual-editor-api-agent: ${err.message}`);
852
+ return reply.code(500).send({ error: err.message });
853
+ }
854
+ });
855
+ // Endpoint to write files to local filesystem (for local mode)
856
+ app.post("/write-files", async (request, reply) => {
857
+ try {
858
+ const { updated_files } = request.body;
859
+ if (!updated_files || typeof updated_files !== "object") {
860
+ return reply.code(400).send({
861
+ error: "Missing or invalid updated_files object",
862
+ });
863
+ }
864
+ const writtenFiles = [];
865
+ const updatedEntries = Object.entries(updated_files);
866
+ for (const [rawPath, newContent] of updatedEntries) {
867
+ try {
868
+ let filePath = rawPath;
869
+ if (filePath.includes(".tmp.")) {
870
+ filePath = filePath.slice(0, filePath.indexOf(".tmp."));
871
+ }
872
+ const targetFilePath = toAbsolutePath(filePath);
873
+ await applyFullFileReplacement(newContent, targetFilePath);
874
+ writtenFiles.push(targetFilePath);
875
+ console.log(`\x1b[32m✓ Wrote ${targetFilePath} to disk\x1b[0m`);
876
+ }
877
+ catch (writeErr) {
878
+ console.error(`\x1b[31m✗ Failed to write ${rawPath}: ${writeErr.message}\x1b[0m`);
879
+ }
880
+ }
881
+ return reply.code(200).send({
882
+ success: true,
883
+ written_files: writtenFiles,
884
+ });
885
+ }
886
+ catch (err) {
887
+ console.error(`\x1b[31m✗ Error in /write-files: ${err.message}\x1b[0m`);
888
+ return reply.code(500).send({ error: err.message });
889
+ }
890
+ });
891
+ return app;
892
+ }
893
+ /**
894
+ * Starts the Codepress development server if not already running
895
+ * @param {Object} options Server configuration options
896
+ * @param {number} [options.port=4321] Port to run the server on
897
+ * @returns {Object|null} The Fastify instance or null if already running
898
+ */
899
+ async function startServer(options = {}) {
900
+ var _a;
901
+ // Only run in development environment
902
+ if (process.env.NODE_ENV === "production") {
903
+ return null;
904
+ }
905
+ // Return existing instance if already running
906
+ if (serverInstance) {
907
+ return serverInstance;
908
+ }
909
+ // Try to acquire lock to ensure only one server instance runs system-wide
910
+ if (!acquireLock()) {
911
+ return null;
912
+ }
913
+ // Get the fixed port
914
+ const port = (_a = options.port) !== null && _a !== void 0 ? _a : getServerPort();
915
+ try {
916
+ // Create the Fastify app
917
+ const app = createApp();
918
+ // Start the server
919
+ await app.listen({ port, host: "0.0.0.0" });
920
+ console.log(`\x1b[32m✅ Codepress Dev Server running at http://localhost:${port}\x1b[0m`);
921
+ // Save instance
922
+ serverInstance = app;
923
+ return app;
924
+ }
925
+ catch (err) {
926
+ if (err.code === "EADDRINUSE") {
927
+ console.log(`\x1b[33mℹ Codepress Dev Server: Port ${port} is already in use, server is likely already running\x1b[0m`);
928
+ }
929
+ else {
930
+ console.error("Codepress Dev Server error:", err);
931
+ }
932
+ return null;
933
+ }
934
+ }
935
+ /**
936
+ * Check if a relative path should be excluded based on ignore patterns.
937
+ * @param {string} relativePath - The path relative to the base directory.
938
+ * @param {RegExp[]} excludePatterns - Compiled ignore patterns.
939
+ * @returns {boolean} True if the path matches any exclude pattern.
940
+ */
941
+ function shouldExclude(relativePath, excludePatterns) {
942
+ return excludePatterns.some((pattern) => pattern.test(relativePath));
943
+ }
944
+ /**
945
+ * Recursively collects file paths from a directory, respecting exclude patterns.
946
+ * @param {string} dir - Directory to traverse.
947
+ * @param {RegExp[]} excludePatterns - Compiled ignore patterns.
948
+ * @param {string} [baseDir=dir] - Base directory used to compute relative paths.
949
+ * @returns {string[]} A list of file paths relative to baseDir.
950
+ */
951
+ function getFilesRecursively(dir, excludePatterns, baseDir = dir) {
952
+ const files = [];
953
+ try {
954
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
955
+ for (const entry of entries) {
956
+ const fullPath = path.join(dir, entry.name);
957
+ const relativePath = path.relative(baseDir, fullPath);
958
+ if (shouldExclude(relativePath, excludePatterns)) {
959
+ continue;
960
+ }
961
+ if (entry.isDirectory()) {
962
+ files.push(...getFilesRecursively(fullPath, excludePatterns, baseDir));
963
+ }
964
+ else if (entry.isFile()) {
965
+ files.push(relativePath);
966
+ }
967
+ }
968
+ }
969
+ catch (error) {
970
+ console.warn(`\x1b[33m⚠ Error reading directory ${dir}: ${error.message}\x1b[0m`);
971
+ }
972
+ return files;
973
+ }
974
+ /**
975
+ * Get a list of files in the current project, respecting gitignore patterns
976
+ * @returns {string} List of file paths, one per line
977
+ */
978
+ function getProjectStructure() {
979
+ try {
980
+ // Read .gitignore patterns
981
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
982
+ let excludePatterns = [
983
+ /^\.git(\/.*)?$/, // Exclude .git directory by default
984
+ ];
985
+ if (fs.existsSync(gitignorePath)) {
986
+ const gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
987
+ const gitignorePatterns = gitignoreContent
988
+ .split("\n")
989
+ .map((line) => line.trim())
990
+ .filter((line) => line && !line.startsWith("#")) // Remove empty lines and comments
991
+ .map((pattern) => {
992
+ // Convert gitignore patterns to regex patterns
993
+ let regexPattern = pattern;
994
+ // Handle negation patterns (starting with !)
995
+ if (pattern.startsWith("!")) {
996
+ // Skip negation patterns for now as they're complex to implement
997
+ return null;
998
+ }
999
+ // Remove leading slash if present (gitignore treats /pattern as root-relative)
1000
+ if (regexPattern.startsWith("/")) {
1001
+ regexPattern = regexPattern.substring(1);
1002
+ }
1003
+ // Remove trailing slash for directories
1004
+ if (regexPattern.endsWith("/")) {
1005
+ regexPattern = regexPattern.substring(0, regexPattern.length - 1);
1006
+ }
1007
+ // Escape special regex characters except * and ?
1008
+ regexPattern = regexPattern
1009
+ .replace(/\./g, "\\.") // Escape dots
1010
+ .replace(/\+/g, "\\+") // Escape plus
1011
+ .replace(/\^/g, "\\^") // Escape caret
1012
+ .replace(/\$/g, "\\$") // Escape dollar
1013
+ .replace(/\(/g, "\\(") // Escape parentheses
1014
+ .replace(/\)/g, "\\)")
1015
+ .replace(/\[/g, "\\[") // Escape brackets
1016
+ .replace(/\]/g, "\\]")
1017
+ .replace(/\{/g, "\\{") // Escape braces
1018
+ .replace(/\}/g, "\\}")
1019
+ .replace(/\|/g, "\\|"); // Escape pipe
1020
+ // Convert gitignore wildcards to regex
1021
+ regexPattern = regexPattern
1022
+ .replace(/\*\*/g, ".*") // ** matches any number of directories
1023
+ .replace(/\*/g, "[^/]*") // * matches anything except path separator
1024
+ .replace(/\?/g, "[^/]"); // ? matches single character except path separator
1025
+ // Create regex pattern for matching file paths
1026
+ if (!regexPattern.includes("/")) {
1027
+ // If no slash, match files/directories at any level
1028
+ regexPattern = `(^|/)${regexPattern}(/.*)?$`;
1029
+ }
1030
+ else {
1031
+ // If contains slash, match from start
1032
+ regexPattern = `^${regexPattern}(/.*)?$`;
1033
+ }
1034
+ try {
1035
+ return new RegExp(regexPattern);
1036
+ }
1037
+ catch (error) {
1038
+ console.warn(`\x1b[33m⚠ Invalid regex pattern for "${pattern}": ${error.message}\x1b[0m`);
1039
+ return null;
1040
+ }
1041
+ })
1042
+ .filter((regex) => regex !== null); // Remove null entries with type guard
1043
+ // Combine default patterns with gitignore patterns
1044
+ excludePatterns = [...excludePatterns, ...gitignorePatterns];
1045
+ console.log(`\x1b[36mℹ Found ${gitignorePatterns.length} valid gitignore patterns\x1b[0m`);
1046
+ }
1047
+ else {
1048
+ console.log(`\x1b[33m⚠ No .gitignore file found, no exclusions applied\x1b[0m`);
1049
+ }
1050
+ const fileList = getFilesRecursively(process.cwd(), excludePatterns);
1051
+ console.log(`\x1b[36mℹ Generated file list with ${fileList.length} files\x1b[0m`);
1052
+ // Return as a formatted string with one file per line
1053
+ return fileList.sort().join("\n");
1054
+ }
1055
+ catch (error) {
1056
+ console.error(`Error generating project structure: ${error.message}`);
1057
+ return "Unable to generate project structure";
1058
+ }
1059
+ }
1060
+ const serverModule = {
1061
+ startServer,
1062
+ createApp,
1063
+ getProjectStructure,
1064
+ };
1065
+ if (process.env.NODE_ENV !== "production") {
1066
+ (async () => {
1067
+ try {
1068
+ serverModule.server = await startServer();
1069
+ }
1070
+ catch (err) {
1071
+ console.error("Failed to auto-start server:", err);
1072
+ }
1073
+ })();
1074
+ }
1075
+ module.exports = serverModule;
1076
+ //# sourceMappingURL=server.js.map