framer-code-link 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +608 -120
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -3,10 +3,13 @@ import { createRequire } from "node:module";
3
3
  import { Command } from "commander";
4
4
  import fs from "fs/promises";
5
5
  import path from "path";
6
- import { WebSocketServer } from "ws";
7
6
  import { createHash } from "crypto";
8
- import { execSync } from "child_process";
9
- import fs$1 from "fs";
7
+ import { execFile, execSync } from "child_process";
8
+ import nodeFs from "fs";
9
+ import os from "os";
10
+ import { promisify } from "util";
11
+ import https from "node:https";
12
+ import { WebSocketServer } from "ws";
10
13
  import { setupTypeAcquisition } from "@typescript/ata";
11
14
  import ts from "typescript";
12
15
  import { fileURLToPath } from "node:url";
@@ -142,7 +145,7 @@ function pathJoin(...parts) {
142
145
  });
143
146
  return res;
144
147
  }
145
- function normalizePath$1(filePath) {
148
+ function normalizePath(filePath) {
146
149
  if (!filePath) return "";
147
150
  const isAbsolute = filePath.startsWith("/");
148
151
  const segments = filePath.replace(/\\/g, "/").split("/");
@@ -159,12 +162,24 @@ function normalizePath$1(filePath) {
159
162
  if (isAbsolute) return `/${normalized}`;
160
163
  return normalized;
161
164
  }
165
+ /**
166
+ * Use when you only want path normalization.
167
+ * Preserves the caller-provided extension so `Foo.ts` and `Foo.tsx` stay distinct.
168
+ */
162
169
  function normalizeCodeFilePath(filePath) {
163
- const normalized = normalizePath$1(filePath);
170
+ const normalized = normalizePath(filePath);
164
171
  return normalized.startsWith("/") ? normalized.slice(1) : normalized;
165
172
  }
166
- function canonicalFileName(filePath) {
167
- return normalizeCodeFilePath(filePath);
173
+ function ensureExtension(filePath, extension = ".tsx") {
174
+ const normalized = normalizeCodeFilePath(filePath);
175
+ return /\.(tsx?|jsx?|json)$/i.test(normalized) ? normalized : `${normalized}${extension}`;
176
+ }
177
+ /**
178
+ * Use when the path must match the code-file API contract.
179
+ * Normalizes the path and ensures a default `.tsx` extension when one is missing.
180
+ */
181
+ function normalizeCodeFilePathWithExtension(filePath) {
182
+ return ensureExtension(filePath);
168
183
  }
169
184
  function sanitizeFilePath(input, capitalizeReactComponent = true) {
170
185
  const trimmed = input.trim();
@@ -188,7 +203,7 @@ function isSupportedExtension$1(filePath) {
188
203
  * Use this for Map keys on operating systems where "File.tsx" and "file.tsx" are the same file.
189
204
  */
190
205
  function fileKeyForLookup(filePath) {
191
- return canonicalFileName(filePath).toLowerCase();
206
+ return normalizeCodeFilePath(filePath).toLowerCase();
192
207
  }
193
208
  /**
194
209
  * Pluralize a word based on count
@@ -220,6 +235,11 @@ function getPortFromHash(projectHash) {
220
235
  return 3847 + Math.abs(hash) % 250;
221
236
  }
222
237
 
238
+ //#endregion
239
+ //#region ../code-link-shared/src/types.ts
240
+ /** Custom close code sent when a new plugin tab replaces the active one. */
241
+ const CLOSE_CODE_REPLACED = 4001;
242
+
223
243
  //#endregion
224
244
  //#region ../../node_modules/picocolors/picocolors.js
225
245
  var require_picocolors = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -475,28 +495,249 @@ function resetDisconnectState() {
475
495
  hadRecentDisconnect = false;
476
496
  }
477
497
 
498
+ //#endregion
499
+ //#region src/helpers/certs.ts
500
+ /**
501
+ * Certificate management for WSS support.
502
+ *
503
+ * Downloads FiloSottile's mkcert binary on first run, then shells out to it
504
+ * to generate and trust a local CA + server certificate for wss://localhost.
505
+ *
506
+ * The mkcert binary is SHA-256 verified before execution (update
507
+ * MKCERT_CHECKSUMS when bumping MKCERT_VERSION). The CA key is user-only;
508
+ * never share or commit the cert directory.
509
+ *
510
+ * Certs and the mkcert binary are cached in ~/.framer/code-link/.
511
+ */
512
+ const execFileAsync = promisify(execFile);
513
+ /** Keep in sync with MKCERT_CHECKSUMS below. */
514
+ const MKCERT_VERSION = "v1.4.4";
515
+ const CERT_DIR = process.env.FRAMER_CODE_LINK_CERT_DIR ?? path.join(os.homedir(), ".framer", "code-link");
516
+ const MKCERT_BIN_NAME = process.platform === "win32" ? "mkcert.exe" : "mkcert";
517
+ const MKCERT_BIN_PATH = path.join(CERT_DIR, MKCERT_BIN_NAME);
518
+ const ROOT_CA_CERT_PATH = path.join(CERT_DIR, "rootCA.pem");
519
+ const ROOT_CA_KEY_PATH = path.join(CERT_DIR, "rootCA-key.pem");
520
+ const SERVER_KEY_PATH = path.join(CERT_DIR, "localhost-key.pem");
521
+ const SERVER_CERT_PATH = path.join(CERT_DIR, "localhost.pem");
522
+ /**
523
+ * SHA-256 checksums for mkcert v1.4.4 release binaries, keyed by "platform-arch".
524
+ * These must be updated whenever MKCERT_VERSION changes.
525
+ * Source: https://github.com/FiloSottile/mkcert/releases/tag/v1.4.4
526
+ */
527
+ const MKCERT_CHECKSUMS = {
528
+ "darwin-amd64": "a32dfab51f1845d51e810db8e47dcf0e6b51ae3422426514bf5a2b8302e97d4e",
529
+ "darwin-arm64": "c8af0df44bce04359794dad8ea28d750437411d632748049d08644ffb66a60c6",
530
+ "linux-amd64": "6d31c65b03972c6dc4a14ab429f2928300518b26503f58723e532d1b0a3bbb52",
531
+ "linux-arm64": "b98f2cc69fd9147fe4d405d859c57504571adec0d3611c3eefd04107c7ac00d0",
532
+ "windows-amd64": "d2660b50a9ed59eada480750561c96abc2ed4c9a38c6a24d93e30e0977631398",
533
+ "windows-arm64": "793747256c562622d40127c8080df26add2fb44c50906ce9db63b42a5280582e"
534
+ };
535
+ /** Env vars passed to every mkcert invocation. */
536
+ const MKCERT_ENV = {
537
+ ...process.env,
538
+ CAROOT: CERT_DIR,
539
+ JAVA_HOME: "",
540
+ ...process.platform === "darwin" ? { TRUST_STORES: "system" } : {}
541
+ };
542
+ /**
543
+ * Returns a TLS cert bundle for the WSS server, or null if generation fails.
544
+ * On first run, downloads mkcert, installs a local CA into trust stores, and
545
+ * generates a server cert for localhost.
546
+ */
547
+ async function getOrCreateCerts() {
548
+ try {
549
+ await fs.mkdir(CERT_DIR, { recursive: true });
550
+ const mkcertPath = await ensureMkcertBinary();
551
+ const rootCAState = await syncRootCA(mkcertPath);
552
+ if (rootCAState !== "unchanged") await invalidateServerCerts(rootCAState);
553
+ const existingKey = await loadFile(SERVER_KEY_PATH);
554
+ const existingCert = await loadFile(SERVER_CERT_PATH);
555
+ if (existingKey && existingCert) {
556
+ debug("Loaded existing server certificates from disk");
557
+ return {
558
+ key: existingKey,
559
+ cert: existingCert
560
+ };
561
+ }
562
+ if (existingKey || existingCert) await invalidateIncompleteServerBundle();
563
+ status("Generating local certificates to connect securely. You may be asked for your password.");
564
+ await generateCerts(mkcertPath);
565
+ return {
566
+ key: await fs.readFile(SERVER_KEY_PATH, "utf-8"),
567
+ cert: await fs.readFile(SERVER_CERT_PATH, "utf-8")
568
+ };
569
+ } catch (err) {
570
+ error(`Failed to set up TLS certificates: ${err instanceof Error ? err.message : String(err)}`);
571
+ return null;
572
+ }
573
+ }
574
+ function getDownloadInfo() {
575
+ const platformMap = {
576
+ darwin: "darwin",
577
+ linux: "linux",
578
+ win32: "windows"
579
+ };
580
+ const archMap = {
581
+ x64: "amd64",
582
+ arm64: "arm64"
583
+ };
584
+ const platform = platformMap[process.platform];
585
+ const arch = archMap[process.arch];
586
+ if (!platform || !arch) throw new Error(`Unsupported platform: ${process.platform}/${process.arch}. Install mkcert manually: https://github.com/FiloSottile/mkcert#installation`);
587
+ const key = `${platform}-${arch}`;
588
+ const expectedChecksum = MKCERT_CHECKSUMS[key];
589
+ if (!expectedChecksum) throw new Error(`No checksum available for mkcert ${key}. Install mkcert manually: https://github.com/FiloSottile/mkcert#installation`);
590
+ return {
591
+ url: `https://github.com/FiloSottile/mkcert/releases/download/${MKCERT_VERSION}/${`mkcert-${MKCERT_VERSION}-${platform}-${arch}${process.platform === "win32" ? ".exe" : ""}`}`,
592
+ expectedChecksum
593
+ };
594
+ }
595
+ async function ensureMkcertBinary() {
596
+ const { url, expectedChecksum } = getDownloadInfo();
597
+ try {
598
+ await fs.access(MKCERT_BIN_PATH, nodeFs.constants.X_OK);
599
+ if (await verifyFileChecksum(MKCERT_BIN_PATH, expectedChecksum)) {
600
+ debug("mkcert binary already available and verified");
601
+ return MKCERT_BIN_PATH;
602
+ }
603
+ warn("Cached mkcert binary failed checksum verification, re-downloading...");
604
+ } catch {}
605
+ debug(`Downloading mkcert from ${url}`);
606
+ status("Downloading mkcert for certificate generation...");
607
+ try {
608
+ const response = await fetch(url, { redirect: "follow" });
609
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
610
+ const buffer = Buffer.from(await response.arrayBuffer());
611
+ const actualChecksum = createHash("sha256").update(buffer).digest("hex");
612
+ if (actualChecksum !== expectedChecksum) throw new Error(`mkcert binary checksum mismatch — the download may have been tampered with.\n Expected: ${expectedChecksum}\n Actual: ${actualChecksum}`);
613
+ await fs.writeFile(MKCERT_BIN_PATH, buffer, { mode: 493 });
614
+ debug(`mkcert binary saved to ${MKCERT_BIN_PATH}`);
615
+ return MKCERT_BIN_PATH;
616
+ } catch (err) {
617
+ await fs.rm(MKCERT_BIN_PATH, { force: true });
618
+ const message = err instanceof Error ? err.message : String(err);
619
+ throw new Error(`Failed to download mkcert: ${message}\nYou can install it manually: https://github.com/FiloSottile/mkcert#installation\nThen run: mkcert -install && mkcert -key-file "${SERVER_KEY_PATH}" -cert-file "${SERVER_CERT_PATH}" localhost 127.0.0.1`);
620
+ }
621
+ }
622
+ async function generateCerts(mkcertPath) {
623
+ debug("Running mkcert to install the local root CA...");
624
+ try {
625
+ await execFileAsync(mkcertPath, ["-install"], { env: MKCERT_ENV });
626
+ } catch (err) {
627
+ throw new Error(`Failed to install mkcert root CA into the system trust store. If you canceled the password prompt, rerun this command and allow the install.
628
+ mkcert error: ${formatMkcertError(err)}`);
629
+ }
630
+ debug("Running mkcert to generate the localhost server certificate...");
631
+ try {
632
+ await execFileAsync(mkcertPath, [
633
+ "-key-file",
634
+ SERVER_KEY_PATH,
635
+ "-cert-file",
636
+ SERVER_CERT_PATH,
637
+ "localhost",
638
+ "127.0.0.1"
639
+ ], { env: MKCERT_ENV });
640
+ } catch (err) {
641
+ if (await loadFile(SERVER_KEY_PATH) || await loadFile(SERVER_CERT_PATH)) await invalidateIncompleteServerBundle();
642
+ throw new Error(`Failed to generate localhost TLS certificate and key with mkcert.
643
+ mkcert error: ${formatMkcertError(err)}\nPlease rerun:\n mkcert -key-file "${SERVER_KEY_PATH}" -cert-file "${SERVER_CERT_PATH}" localhost 127.0.0.1`);
644
+ }
645
+ const [generatedKey, generatedCert] = await Promise.all([loadFile(SERVER_KEY_PATH), loadFile(SERVER_CERT_PATH)]);
646
+ if (generatedKey && generatedCert) {
647
+ debug("CA installed and server certificate generated successfully");
648
+ return;
649
+ }
650
+ if (generatedKey || generatedCert) await invalidateIncompleteServerBundle();
651
+ throw new Error(`Failed to generate localhost TLS certificate and key with mkcert. Please ensure mkcert is installed and rerun:
652
+ mkcert -install && mkcert -key-file "${SERVER_KEY_PATH}" -cert-file "${SERVER_CERT_PATH}" localhost 127.0.0.1`);
653
+ }
654
+ async function syncRootCA(mkcertPath) {
655
+ const existingRootCert = await loadFile(ROOT_CA_CERT_PATH);
656
+ const existingRootKey = await loadFile(ROOT_CA_KEY_PATH);
657
+ const { stdout } = await execFileAsync(mkcertPath, ["-CAROOT"], { env: {
658
+ ...process.env,
659
+ JAVA_HOME: ""
660
+ } });
661
+ const defaultCAROOT = stdout.trim();
662
+ if (!defaultCAROOT || defaultCAROOT === CERT_DIR) return existingRootCert && existingRootKey ? "unchanged" : "missing";
663
+ const defaultRootCert = await loadFile(path.join(defaultCAROOT, "rootCA.pem"));
664
+ const defaultRootKey = await loadFile(path.join(defaultCAROOT, "rootCA-key.pem"));
665
+ if (!defaultRootCert || !defaultRootKey) return existingRootCert && existingRootKey ? "unchanged" : "missing";
666
+ if (existingRootCert === defaultRootCert && existingRootKey === defaultRootKey) return "unchanged";
667
+ await Promise.all([fs.rm(ROOT_CA_CERT_PATH, { force: true }), fs.rm(ROOT_CA_KEY_PATH, { force: true })]);
668
+ await fs.writeFile(ROOT_CA_CERT_PATH, defaultRootCert, { mode: 420 });
669
+ await fs.writeFile(ROOT_CA_KEY_PATH, defaultRootKey, { mode: 384 });
670
+ return existingRootCert && existingRootKey ? "updated" : "copied";
671
+ }
672
+ async function invalidateServerCerts(rootCAState) {
673
+ const reasons = {
674
+ copied: "Copied an existing mkcert root CA into the Code Link cache",
675
+ updated: "Detected a different mkcert root CA and refreshed the Code Link cache",
676
+ missing: "No cached mkcert root CA was available for the existing server certificate"
677
+ };
678
+ if (!(await loadFile(SERVER_KEY_PATH) !== null || await loadFile(SERVER_CERT_PATH) !== null)) return;
679
+ await fs.rm(SERVER_KEY_PATH, { force: true });
680
+ await fs.rm(SERVER_CERT_PATH, { force: true });
681
+ debug(`${reasons[rootCAState]}; removed stale localhost certificate`);
682
+ }
683
+ async function invalidateIncompleteServerBundle() {
684
+ await fs.rm(SERVER_KEY_PATH, { force: true });
685
+ await fs.rm(SERVER_CERT_PATH, { force: true });
686
+ warn("Found an incomplete localhost certificate bundle; regenerating it");
687
+ }
688
+ async function verifyFileChecksum(filePath, expectedHash) {
689
+ const data = await fs.readFile(filePath);
690
+ return createHash("sha256").update(data).digest("hex") === expectedHash;
691
+ }
692
+ async function loadFile(filePath) {
693
+ try {
694
+ return await fs.readFile(filePath, "utf-8");
695
+ } catch {
696
+ return null;
697
+ }
698
+ }
699
+ function formatMkcertError(err) {
700
+ if (err instanceof Error) {
701
+ const stdout = "stdout" in err && typeof err.stdout === "string" ? err.stdout.trim() : "";
702
+ const output = ["stderr" in err && typeof err.stderr === "string" ? err.stderr.trim() : "", stdout].filter(Boolean).join("\n");
703
+ return output ? `${err.message}\n${output}` : err.message;
704
+ }
705
+ return String(err);
706
+ }
707
+
478
708
  //#endregion
479
709
  //#region src/helpers/connection.ts
480
710
  /**
481
- * Initializes a WebSocket server and returns a connection interface
482
- * Returns a Promise that resolves when the server is ready, or rejects on startup errors
711
+ * WebSocket connection helper
712
+ *
713
+ * Wrapper around ws.Server that normalizes handshake and surfaces callbacks.
714
+ */
715
+ /**
716
+ * Initializes a WSS (TLS) WebSocket server and returns a connection interface.
717
+ * Returns a Promise that resolves when the server is ready, or rejects on startup errors.
483
718
  */
484
- function initConnection(port) {
719
+ function initConnection(port, certs) {
485
720
  return new Promise((resolve, reject) => {
486
- const wss = new WebSocketServer({ port });
487
721
  const handlers = {};
488
722
  let connectionId = 0;
489
723
  let isReady = false;
724
+ const httpsServer = https.createServer({
725
+ key: certs.key,
726
+ cert: certs.cert
727
+ });
728
+ const wss = new WebSocketServer({ server: httpsServer });
490
729
  wss.on("error", (err) => {
730
+ error(`WebSocket server error: ${err.message}`);
731
+ handlers.onError?.(err);
732
+ });
733
+ const handleError = (err) => {
491
734
  if (!isReady) {
492
735
  if (err.code === "EADDRINUSE") {
493
736
  error(`Port ${port} is already in use.`);
494
- error(`This usually means another instance of Code Link is already running.`);
495
- error(``);
496
- error(`To fix this:`);
497
- error(` 1. Close any other terminal running Code Link for this project`);
498
- error(` 2. Or run: lsof -i :${port} | grep LISTEN`);
499
- error(` Then kill the process: kill -9 <PID>`);
737
+ info(`This usually means another instance of Code Link is already running.`);
738
+ info(``);
739
+ info(`To fix this:`);
740
+ info(` Close any other terminal running Code Link for this project`);
500
741
  reject(/* @__PURE__ */ new Error(`Port ${port} is already in use`));
501
742
  } else {
502
743
  error(`Failed to start WebSocket server: ${err.message}`);
@@ -505,10 +746,11 @@ function initConnection(port) {
505
746
  return;
506
747
  }
507
748
  error(`WebSocket server error: ${err.message}`);
508
- });
509
- wss.on("listening", () => {
749
+ handlers.onError?.(err);
750
+ };
751
+ const handleListening = () => {
510
752
  isReady = true;
511
- debug(`WebSocket server listening on port ${port}`);
753
+ debug(`WSS server listening on port ${port}`);
512
754
  let activeClient = null;
513
755
  wss.on("connection", (ws) => {
514
756
  const connId = ++connectionId;
@@ -524,7 +766,7 @@ function initConnection(port) {
524
766
  activeClient = ws;
525
767
  if (previousActiveClient && previousActiveClient !== activeClient) {
526
768
  debug(`Replacing active client with conn ${connId}`);
527
- if (previousActiveClient.readyState === READY_STATE.OPEN || previousActiveClient.readyState === READY_STATE.CONNECTING) previousActiveClient.close();
769
+ if (previousActiveClient.readyState === READY_STATE.OPEN || previousActiveClient.readyState === READY_STATE.CONNECTING) previousActiveClient.close(CLOSE_CODE_REPLACED);
528
770
  }
529
771
  handlers.onHandshake?.(ws, message);
530
772
  } else if (handshakeReceived && activeClient === ws) handlers.onMessage?.(message);
@@ -564,9 +806,13 @@ function initConnection(port) {
564
806
  },
565
807
  close() {
566
808
  wss.close();
809
+ httpsServer.close();
567
810
  }
568
811
  });
569
- });
812
+ };
813
+ httpsServer.on("error", handleError);
814
+ httpsServer.on("listening", handleListening);
815
+ httpsServer.listen(port);
570
816
  });
571
817
  }
572
818
  /**
@@ -608,41 +854,6 @@ function sendMessage(socket, message) {
608
854
  });
609
855
  }
610
856
 
611
- //#endregion
612
- //#region src/utils/node-paths.ts
613
- /**
614
- * Path manipulation utilities
615
- */
616
- /**
617
- * Gets a relative path from the project directory
618
- */
619
- function getRelativePath(projectDir, absolutePath) {
620
- return path.relative(projectDir, absolutePath);
621
- }
622
- /**
623
- * Normalizes a file path by:
624
- * - Converting backslashes to forward slashes
625
- * - Resolving . and .. segments
626
- * - Removing duplicate slashes
627
- */
628
- function normalizePath(filePath) {
629
- if (!filePath) return "";
630
- const isAbsolute = filePath.startsWith("/");
631
- const segments = filePath.replace(/\\/g, "/").split("/");
632
- const stack = [];
633
- for (const segment of segments) {
634
- if (!segment || segment === ".") continue;
635
- if (segment === "..") {
636
- if (stack.length > 0) stack.pop();
637
- continue;
638
- }
639
- stack.push(segment);
640
- }
641
- const normalized = stack.join("/");
642
- if (isAbsolute) return `/${normalized}`;
643
- return normalized;
644
- }
645
-
646
857
  //#endregion
647
858
  //#region src/utils/state-persistence.ts
648
859
  /**
@@ -654,18 +865,8 @@ function normalizePath(filePath) {
654
865
  */
655
866
  const STATE_FILE_NAME = ".framer-sync-state.json";
656
867
  const CURRENT_VERSION = 1;
657
- const SUPPORTED_EXTENSIONS$1 = [
658
- ".ts",
659
- ".tsx",
660
- ".js",
661
- ".jsx",
662
- ".json"
663
- ];
664
- const DEFAULT_EXTENSION$1 = ".tsx";
665
- function normalizePersistedFileName(fileName) {
666
- let normalized = normalizePath(fileName.trim());
667
- if (!SUPPORTED_EXTENSIONS$1.some((ext) => normalized.toLowerCase().endsWith(ext))) normalized = `${normalized}${DEFAULT_EXTENSION$1}`;
668
- return normalized;
868
+ function persistedFileKey(fileName) {
869
+ return fileKeyForLookup(normalizeCodeFilePathWithExtension(fileName));
669
870
  }
670
871
  /**
671
872
  * Hash file content to detect changes
@@ -687,7 +888,7 @@ async function loadPersistedState(projectDir) {
687
888
  return result;
688
889
  }
689
890
  for (const [fileName, state] of Object.entries(parsed.files)) {
690
- const normalizedName = normalizePersistedFileName(fileName);
891
+ const normalizedName = persistedFileKey(fileName);
691
892
  if (normalizedName !== fileName) debug(`Normalized persisted key "${fileName}" -> "${normalizedName}" for compatibility`);
692
893
  result.set(normalizedName, state);
693
894
  }
@@ -755,7 +956,7 @@ async function listFiles(filesDir) {
755
956
  continue;
756
957
  }
757
958
  if (!isSupportedExtension(entry.name)) continue;
758
- const sanitizedPath = sanitizeFilePath(normalizePath$1(path.relative(filesDir, entryPath)), false).path;
959
+ const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
759
960
  try {
760
961
  const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
761
962
  files.push({
@@ -1000,9 +1201,9 @@ function resolveRemoteReference(filesDir, rawName) {
1000
1201
  };
1001
1202
  }
1002
1203
  function sanitizeRelativePath(relativePath) {
1003
- const trimmed = normalizePath$1(relativePath.trim());
1204
+ const trimmed = normalizePath(relativePath.trim());
1004
1205
  const sanitized = sanitizeFilePath(SUPPORTED_EXTENSIONS.some((ext) => trimmed.toLowerCase().endsWith(ext)) ? trimmed : `${trimmed}${DEFAULT_EXTENSION}`, false);
1005
- const normalized = normalizePath$1(sanitized.path);
1206
+ const normalized = normalizePath(sanitized.path);
1006
1207
  return {
1007
1208
  relativePath: normalized,
1008
1209
  extension: sanitized.extension || path.extname(normalized) || DEFAULT_EXTENSION
@@ -1061,6 +1262,7 @@ function tryGitInit(projectDir) {
1061
1262
  debug("Already in a repository, skipping git init");
1062
1263
  return false;
1063
1264
  }
1265
+ status("Initializing git repository...");
1064
1266
  execSync("git init", {
1065
1267
  stdio: "ignore",
1066
1268
  cwd: projectDir
@@ -1082,7 +1284,7 @@ function tryGitInit(projectDir) {
1082
1284
  return true;
1083
1285
  } catch (e) {
1084
1286
  if (didInit) try {
1085
- fs$1.rmSync(path.join(projectDir, ".git"), {
1287
+ nodeFs.rmSync(path.join(projectDir, ".git"), {
1086
1288
  recursive: true,
1087
1289
  force: true
1088
1290
  });
@@ -1379,7 +1581,8 @@ var Installer = class {
1379
1581
  try {
1380
1582
  await this.ata(filteredContent);
1381
1583
  } catch (err) {
1382
- warn(`ATA failed for ${fileName}`, err);
1584
+ warn(`Type fetching failed for ${fileName}`);
1585
+ debug(`ATA error for ${fileName}:`, err);
1383
1586
  }
1384
1587
  }
1385
1588
  /**
@@ -1502,7 +1705,7 @@ declare module "*.json"
1502
1705
  private: true,
1503
1706
  description: "Framer files synced with framer-code-link"
1504
1707
  };
1505
- await fs.writeFile(packagePath, JSON.stringify(pkg, null, 2));
1708
+ await fs.writeFile(packagePath, JSON.stringify(pkg, null, 4));
1506
1709
  debug("Created package.json");
1507
1710
  }
1508
1711
  }
@@ -1634,11 +1837,12 @@ async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
1634
1837
  if (isRetryable) checkFatalFailure(urlString);
1635
1838
  if (attempt < retries && isRetryable) {
1636
1839
  const delay = attempt * 1e3;
1637
- warn(`Fetch failed (${error.cause?.code ?? error.message}) for ${urlString}, retrying in ${delay}ms...`);
1840
+ debug(`Fetch failed for ${urlString}, retrying...`, error);
1638
1841
  await new Promise((resolve) => setTimeout(resolve, delay));
1639
1842
  continue;
1640
1843
  }
1641
- warn(`Fetch failed for ${urlString}`, error);
1844
+ warn(`Fetch failed for ${urlString}`);
1845
+ debug(`Fetch error details:`, error);
1642
1846
  throw error;
1643
1847
  }
1644
1848
  }
@@ -1783,6 +1987,18 @@ function validateIncomingChange(fileMeta, currentMode) {
1783
1987
  };
1784
1988
  }
1785
1989
 
1990
+ //#endregion
1991
+ //#region src/utils/node-paths.ts
1992
+ /**
1993
+ * Path manipulation utilities
1994
+ */
1995
+ /**
1996
+ * Gets a relative path from the project directory
1997
+ */
1998
+ function getRelativePath(projectDir, absolutePath) {
1999
+ return path.relative(projectDir, absolutePath);
2000
+ }
2001
+
1786
2002
  //#endregion
1787
2003
  //#region src/helpers/watcher.ts
1788
2004
  /**
@@ -1790,47 +2006,244 @@ function validateIncomingChange(fileMeta, currentMode) {
1790
2006
  *
1791
2007
  * Wrapper around chokidar that normalizes file paths and filters to ts, tsx, js, json.
1792
2008
  */
2009
+ const RENAME_BUFFER_MS = 100;
2010
+ function findUniqueHashMatch(pendingItems, contentHash) {
2011
+ let matchingKey;
2012
+ for (const [key, pending] of pendingItems) {
2013
+ if (pending.contentHash !== contentHash) continue;
2014
+ if (matchingKey !== void 0) return;
2015
+ matchingKey = key;
2016
+ }
2017
+ return matchingKey;
2018
+ }
2019
+ function matchPendingAddForDelete(contentHash, pendingAdds) {
2020
+ if (!contentHash) return null;
2021
+ const matchingAddKey = findUniqueHashMatch(pendingAdds, contentHash);
2022
+ if (!matchingAddKey) return null;
2023
+ const pendingAdd = pendingAdds.get(matchingAddKey);
2024
+ if (!pendingAdd) return null;
2025
+ return {
2026
+ key: matchingAddKey,
2027
+ pendingAdd
2028
+ };
2029
+ }
2030
+ function matchPendingDeleteForAdd(contentHash, pendingDeletes) {
2031
+ const matchingDeleteKey = findUniqueHashMatch(pendingDeletes, contentHash);
2032
+ if (!matchingDeleteKey) return null;
2033
+ const pendingDelete = pendingDeletes.get(matchingDeleteKey);
2034
+ if (!pendingDelete) return null;
2035
+ return {
2036
+ key: matchingDeleteKey,
2037
+ pendingDelete
2038
+ };
2039
+ }
1793
2040
  /**
1794
2041
  * Initializes a file watcher for the given directory
1795
2042
  */
1796
2043
  function initWatcher(filesDir) {
1797
2044
  const handlers = [];
2045
+ const contentHashCache = /* @__PURE__ */ new Map();
2046
+ const pendingDeletes = /* @__PURE__ */ new Map();
2047
+ const pendingAdds = /* @__PURE__ */ new Map();
2048
+ const recentSanitizations = /* @__PURE__ */ new Set();
1798
2049
  const watcher = chokidar.watch(filesDir, {
1799
2050
  ignored: /(^|[/\\])\.\./,
1800
2051
  persistent: true,
1801
2052
  ignoreInitial: false
1802
2053
  });
1803
2054
  debug(`Watching directory: ${filesDir}`);
1804
- const emitEvent = async (kind, absolutePath) => {
1805
- if (!isSupportedExtension$1(absolutePath)) return;
1806
- const rawRelativePath = normalizePath$1(getRelativePath(filesDir, absolutePath));
1807
- const relativePath = sanitizeFilePath(rawRelativePath, false).path;
2055
+ const dispatchEvent = (event) => {
2056
+ let eventToDispatch = event;
2057
+ if (event.kind === "rename" && event.relativePath === event.oldRelativePath) {
2058
+ if (event.content === void 0) {
2059
+ warn(`Skipping invalid same-path rename without content: ${event.relativePath}`);
2060
+ return;
2061
+ }
2062
+ debug(`Converting same-path rename to change: ${event.relativePath}`);
2063
+ eventToDispatch = {
2064
+ kind: "change",
2065
+ relativePath: event.relativePath,
2066
+ content: event.content
2067
+ };
2068
+ }
2069
+ debug(`Watcher event: ${eventToDispatch.kind} ${eventToDispatch.relativePath}`);
2070
+ for (const handler of handlers) handler(eventToDispatch);
2071
+ };
2072
+ /**
2073
+ * Resolves the relative path identity for a watcher event.
2074
+ * Only "add" may rewrite that identity by successfully sanitizing on disk.
2075
+ */
2076
+ const resolveRelativePath = async (kind, absolutePath) => {
2077
+ if (!isSupportedExtension$1(absolutePath)) return null;
2078
+ const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
2079
+ let relativePath = rawRelativePath;
1808
2080
  let effectiveAbsolutePath = absolutePath;
1809
- if (relativePath !== rawRelativePath && kind === "add") {
1810
- const newAbsolutePath = path.join(filesDir, relativePath);
1811
- try {
1812
- await fs.mkdir(path.dirname(newAbsolutePath), { recursive: true });
1813
- await fs.rename(absolutePath, newAbsolutePath);
1814
- debug(`Renamed ${rawRelativePath} -> ${relativePath}`);
1815
- effectiveAbsolutePath = newAbsolutePath;
1816
- } catch (err) {
1817
- warn(`Failed to rename ${rawRelativePath}`, err);
2081
+ if (kind === "add") {
2082
+ const sanitized = sanitizeFilePath(rawRelativePath, false);
2083
+ if (sanitized.path !== rawRelativePath) {
2084
+ const nextRelativePath = sanitized.path;
2085
+ const newAbsolutePath = path.join(filesDir, nextRelativePath);
2086
+ try {
2087
+ await fs.mkdir(path.dirname(newAbsolutePath), { recursive: true });
2088
+ await fs.rename(absolutePath, newAbsolutePath);
2089
+ debug(`Renamed ${rawRelativePath} -> ${nextRelativePath}`);
2090
+ relativePath = nextRelativePath;
2091
+ effectiveAbsolutePath = newAbsolutePath;
2092
+ recentSanitizations.add(rawRelativePath);
2093
+ recentSanitizations.add(nextRelativePath);
2094
+ setTimeout(() => {
2095
+ recentSanitizations.delete(rawRelativePath);
2096
+ recentSanitizations.delete(nextRelativePath);
2097
+ }, RENAME_BUFFER_MS * 3);
2098
+ } catch (err) {
2099
+ warn(`Failed to rename ${rawRelativePath}`, err);
2100
+ return {
2101
+ relativePath: rawRelativePath,
2102
+ effectiveAbsolutePath: absolutePath
2103
+ };
2104
+ }
1818
2105
  }
1819
2106
  }
2107
+ return {
2108
+ relativePath,
2109
+ effectiveAbsolutePath
2110
+ };
2111
+ };
2112
+ const emitEvent = async (kind, absolutePath) => {
2113
+ const rawRelPath = normalizePath(getRelativePath(filesDir, absolutePath));
2114
+ if (recentSanitizations.delete(rawRelPath)) {
2115
+ debug(`Suppressing sanitization echo: ${kind} ${rawRelPath}`);
2116
+ return;
2117
+ }
2118
+ const resolved = await resolveRelativePath(kind, absolutePath);
2119
+ if (!resolved) return;
2120
+ const { relativePath, effectiveAbsolutePath } = resolved;
2121
+ if (kind === "delete") {
2122
+ const lastHash = contentHashCache.get(relativePath);
2123
+ contentHashCache.delete(relativePath);
2124
+ const samePathPendingAdd = pendingAdds.get(relativePath);
2125
+ if (samePathPendingAdd) {
2126
+ clearTimeout(samePathPendingAdd.timer);
2127
+ pendingAdds.delete(relativePath);
2128
+ try {
2129
+ const latestContent = await fs.readFile(effectiveAbsolutePath, "utf-8");
2130
+ const latestHash = hashFileContent(latestContent);
2131
+ contentHashCache.set(relativePath, latestHash);
2132
+ dispatchEvent({
2133
+ kind: "change",
2134
+ relativePath,
2135
+ content: latestContent
2136
+ });
2137
+ } catch {
2138
+ if (samePathPendingAdd.previousContentHash !== void 0) dispatchEvent({
2139
+ kind: "delete",
2140
+ relativePath
2141
+ });
2142
+ else debug(`Suppressing transient add+delete: ${relativePath}`);
2143
+ }
2144
+ return;
2145
+ }
2146
+ const matchedAdd = matchPendingAddForDelete(lastHash, pendingAdds);
2147
+ if (matchedAdd) {
2148
+ clearTimeout(matchedAdd.pendingAdd.timer);
2149
+ pendingAdds.delete(matchedAdd.key);
2150
+ dispatchEvent({
2151
+ kind: "rename",
2152
+ relativePath: matchedAdd.pendingAdd.relativePath,
2153
+ oldRelativePath: relativePath,
2154
+ content: matchedAdd.pendingAdd.content
2155
+ });
2156
+ return;
2157
+ }
2158
+ if (lastHash) {
2159
+ const timer = setTimeout(() => {
2160
+ pendingDeletes.delete(relativePath);
2161
+ dispatchEvent({
2162
+ kind: "delete",
2163
+ relativePath
2164
+ });
2165
+ }, RENAME_BUFFER_MS);
2166
+ pendingDeletes.set(relativePath, {
2167
+ relativePath,
2168
+ contentHash: lastHash,
2169
+ timer
2170
+ });
2171
+ } else dispatchEvent({
2172
+ kind: "delete",
2173
+ relativePath
2174
+ });
2175
+ return;
2176
+ }
1820
2177
  let content;
1821
- if (kind !== "delete") try {
2178
+ try {
1822
2179
  content = await fs.readFile(effectiveAbsolutePath, "utf-8");
1823
2180
  } catch (err) {
1824
2181
  debug(`Failed to read file ${relativePath}:`, err);
1825
2182
  return;
1826
2183
  }
1827
- const event = {
2184
+ const previousContentHash = contentHashCache.get(relativePath);
2185
+ const contentHash = hashFileContent(content);
2186
+ contentHashCache.set(relativePath, contentHash);
2187
+ if (kind === "add") {
2188
+ const samePathPendingDelete = pendingDeletes.get(relativePath);
2189
+ if (samePathPendingDelete) {
2190
+ clearTimeout(samePathPendingDelete.timer);
2191
+ pendingDeletes.delete(relativePath);
2192
+ dispatchEvent({
2193
+ kind: "change",
2194
+ relativePath,
2195
+ content
2196
+ });
2197
+ return;
2198
+ }
2199
+ const matchedDelete = matchPendingDeleteForAdd(contentHash, pendingDeletes);
2200
+ if (matchedDelete) {
2201
+ clearTimeout(matchedDelete.pendingDelete.timer);
2202
+ pendingDeletes.delete(matchedDelete.key);
2203
+ dispatchEvent({
2204
+ kind: "rename",
2205
+ relativePath,
2206
+ oldRelativePath: matchedDelete.pendingDelete.relativePath,
2207
+ content
2208
+ });
2209
+ return;
2210
+ }
2211
+ const existingPendingAdd = pendingAdds.get(relativePath);
2212
+ if (existingPendingAdd) clearTimeout(existingPendingAdd.timer);
2213
+ const retainedPreviousContentHash = existingPendingAdd ? existingPendingAdd.previousContentHash : previousContentHash;
2214
+ const timer = setTimeout(() => {
2215
+ pendingAdds.delete(relativePath);
2216
+ dispatchEvent({
2217
+ kind: "add",
2218
+ relativePath,
2219
+ content
2220
+ });
2221
+ }, RENAME_BUFFER_MS);
2222
+ pendingAdds.set(relativePath, {
2223
+ relativePath,
2224
+ contentHash,
2225
+ content,
2226
+ timer,
2227
+ previousContentHash: retainedPreviousContentHash
2228
+ });
2229
+ return;
2230
+ }
2231
+ const pendingAdd = pendingAdds.get(relativePath);
2232
+ if (pendingAdd) {
2233
+ clearTimeout(pendingAdd.timer);
2234
+ pendingAdds.delete(relativePath);
2235
+ dispatchEvent({
2236
+ kind: "add",
2237
+ relativePath,
2238
+ content
2239
+ });
2240
+ return;
2241
+ }
2242
+ dispatchEvent({
1828
2243
  kind,
1829
2244
  relativePath,
1830
2245
  content
1831
- };
1832
- debug(`Watcher event: ${kind} ${relativePath}`);
1833
- for (const handler of handlers) handler(event);
2246
+ });
1834
2247
  };
1835
2248
  watcher.on("add", (filePath) => {
1836
2249
  emitEvent("add", filePath);
@@ -1846,6 +2259,12 @@ function initWatcher(filesDir) {
1846
2259
  handlers.push(handler);
1847
2260
  },
1848
2261
  async close() {
2262
+ for (const pending of pendingDeletes.values()) clearTimeout(pending.timer);
2263
+ for (const pending of pendingAdds.values()) clearTimeout(pending.timer);
2264
+ pendingDeletes.clear();
2265
+ pendingAdds.clear();
2266
+ contentHashCache.clear();
2267
+ recentSanitizations.clear();
1849
2268
  await watcher.close();
1850
2269
  }
1851
2270
  };
@@ -1948,45 +2367,42 @@ var FileMetadataCache = class {
1948
2367
  function createHashTracker() {
1949
2368
  const hashes = /* @__PURE__ */ new Map();
1950
2369
  const pendingDeletes = /* @__PURE__ */ new Map();
2370
+ const keyFor = (filePath) => normalizeCodeFilePathWithExtension(filePath);
1951
2371
  return {
1952
2372
  remember(filePath, content) {
1953
- const hash = hashContent(content);
1954
- hashes.set(filePath, hash);
2373
+ const hash = hashFileContent(content);
2374
+ hashes.set(keyFor(filePath), hash);
1955
2375
  },
1956
2376
  shouldSkip(filePath, content) {
1957
- const currentHash = hashContent(content);
1958
- return hashes.get(filePath) === currentHash;
2377
+ const currentHash = hashFileContent(content);
2378
+ return hashes.get(keyFor(filePath)) === currentHash;
1959
2379
  },
1960
2380
  forget(filePath) {
1961
- hashes.delete(filePath);
2381
+ hashes.delete(keyFor(filePath));
1962
2382
  },
1963
2383
  clear() {
1964
2384
  hashes.clear();
1965
2385
  },
1966
2386
  markDelete(filePath) {
1967
- const existingTimer = pendingDeletes.get(filePath);
2387
+ const key = keyFor(filePath);
2388
+ const existingTimer = pendingDeletes.get(key);
1968
2389
  if (existingTimer) clearTimeout(existingTimer);
1969
2390
  const timeout = setTimeout(() => {
1970
- pendingDeletes.delete(filePath);
2391
+ pendingDeletes.delete(key);
1971
2392
  }, 5e3);
1972
- pendingDeletes.set(filePath, timeout);
2393
+ pendingDeletes.set(key, timeout);
1973
2394
  },
1974
2395
  shouldSkipDelete(filePath) {
1975
- return pendingDeletes.has(filePath);
2396
+ return pendingDeletes.has(keyFor(filePath));
1976
2397
  },
1977
2398
  clearDelete(filePath) {
1978
- const timeout = pendingDeletes.get(filePath);
2399
+ const key = keyFor(filePath);
2400
+ const timeout = pendingDeletes.get(key);
1979
2401
  if (timeout) clearTimeout(timeout);
1980
- pendingDeletes.delete(filePath);
2402
+ pendingDeletes.delete(key);
1981
2403
  }
1982
2404
  };
1983
2405
  }
1984
- /**
1985
- * Computes a SHA256 hash of file content for comparison
1986
- */
1987
- function hashContent(content) {
1988
- return createHash("sha256").update(content).digest("hex");
1989
- }
1990
2406
 
1991
2407
  //#endregion
1992
2408
  //#region src/utils/project.ts
@@ -1994,7 +2410,7 @@ function toPackageName(name) {
1994
2410
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
1995
2411
  }
1996
2412
  function toDirectoryName(name) {
1997
- return name.replace(/[^a-zA-Z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
2413
+ return name.replace(/[^a-zA-Z0-9 -]/g, "-").trim().replace(/^-+|-+$/g, "").replace(/-+/g, "-");
1998
2414
  }
1999
2415
  async function getProjectHashFromCwd() {
2000
2416
  try {
@@ -2034,7 +2450,7 @@ async function findOrCreateProjectDirectory(options) {
2034
2450
  shortProjectHash: shortId,
2035
2451
  framerProjectName: projectName
2036
2452
  };
2037
- await fs.writeFile(path.join(projectDirectory, "package.json"), JSON.stringify(pkg, null, 2));
2453
+ await fs.writeFile(path.join(projectDirectory, "package.json"), JSON.stringify(pkg, null, 4));
2038
2454
  return {
2039
2455
  directory: projectDirectory,
2040
2456
  created: true,
@@ -2399,6 +2815,21 @@ function transition(state, event) {
2399
2815
  fileNames: [relativePath]
2400
2816
  });
2401
2817
  break;
2818
+ case "rename":
2819
+ if (content === void 0 || !event.event.oldRelativePath) {
2820
+ effects.push(log("warn", `Rename event missing data: ${relativePath}`));
2821
+ return {
2822
+ state,
2823
+ effects
2824
+ };
2825
+ }
2826
+ effects.push(log("debug", `Local rename detected: ${event.event.oldRelativePath} → ${relativePath}`), {
2827
+ type: "SEND_FILE_RENAME",
2828
+ oldFileName: event.event.oldRelativePath,
2829
+ newFileName: relativePath,
2830
+ content
2831
+ });
2832
+ break;
2402
2833
  }
2403
2834
  return {
2404
2835
  state,
@@ -2487,7 +2918,7 @@ function transition(state, event) {
2487
2918
  * Returns additional events that should be processed (e.g., CONFLICTS_DETECTED after DETECT_CONFLICTS)
2488
2919
  */
2489
2920
  async function executeEffect(effect, context) {
2490
- const { config, hashTracker, installer, fileMetadataCache, userActions, syncState } = context;
2921
+ const { config, hashTracker, installer, fileMetadataCache, pendingRenameConfirmations, userActions, syncState } = context;
2491
2922
  switch (effect.type) {
2492
2923
  case "INIT_WORKSPACE":
2493
2924
  if (!config.projectDir) {
@@ -2582,10 +3013,18 @@ async function executeEffect(effect, context) {
2582
3013
  case "UPDATE_FILE_METADATA": {
2583
3014
  if (!config.filesDir || !config.projectDir) return [];
2584
3015
  const currentContent = await readFileSafe(effect.fileName, config.filesDir);
2585
- if (currentContent !== null) {
2586
- const contentHash = hashFileContent(currentContent);
3016
+ const pendingRenameConfirmation = pendingRenameConfirmations.get(normalizeCodeFilePathWithExtension(effect.fileName));
3017
+ const syncedContent = currentContent ?? pendingRenameConfirmation?.content ?? null;
3018
+ if (syncedContent !== null) {
3019
+ const contentHash = hashFileContent(syncedContent);
2587
3020
  fileMetadataCache.recordSyncedSnapshot(effect.fileName, contentHash, effect.remoteModifiedAt);
2588
3021
  }
3022
+ if (pendingRenameConfirmation) {
3023
+ hashTracker.forget(pendingRenameConfirmation.oldFileName);
3024
+ fileMetadataCache.recordDelete(pendingRenameConfirmation.oldFileName);
3025
+ if (currentContent !== null) hashTracker.remember(effect.fileName, currentContent);
3026
+ pendingRenameConfirmations.delete(normalizeCodeFilePathWithExtension(effect.fileName));
3027
+ }
2589
3028
  return [];
2590
3029
  }
2591
3030
  case "SEND_LOCAL_CHANGE": {
@@ -2612,6 +3051,37 @@ async function executeEffect(effect, context) {
2612
3051
  }
2613
3052
  return [];
2614
3053
  }
3054
+ case "SEND_FILE_RENAME": {
3055
+ const normalizedNewFileName = normalizeCodeFilePathWithExtension(effect.newFileName);
3056
+ if (hashTracker.shouldSkip(normalizedNewFileName, effect.content) && hashTracker.shouldSkipDelete(effect.oldFileName)) {
3057
+ hashTracker.forget(normalizedNewFileName);
3058
+ hashTracker.clearDelete(effect.oldFileName);
3059
+ debug(`Skipping echoed rename ${effect.oldFileName} -> ${effect.newFileName}`);
3060
+ return [];
3061
+ }
3062
+ try {
3063
+ if (!syncState.socket) {
3064
+ warn(`No socket available to send rename ${effect.oldFileName} -> ${effect.newFileName}`);
3065
+ return [];
3066
+ }
3067
+ if (!await sendMessage(syncState.socket, {
3068
+ type: "file-rename",
3069
+ oldFileName: effect.oldFileName,
3070
+ newFileName: normalizedNewFileName,
3071
+ content: effect.content
3072
+ })) {
3073
+ warn(`Failed to send rename ${effect.oldFileName} -> ${effect.newFileName}`);
3074
+ return [];
3075
+ }
3076
+ pendingRenameConfirmations.set(normalizeCodeFilePathWithExtension(effect.newFileName), {
3077
+ oldFileName: effect.oldFileName,
3078
+ content: effect.content
3079
+ });
3080
+ } catch (err) {
3081
+ warn(`Failed to send rename ${effect.oldFileName} -> ${effect.newFileName}`);
3082
+ }
3083
+ return [];
3084
+ }
2615
3085
  case "LOCAL_INITIATED_FILE_DELETE": {
2616
3086
  const filesToDelete = effect.fileNames.filter((fileName) => {
2617
3087
  const shouldSkip = hashTracker.shouldSkipDelete(fileName);
@@ -2682,6 +3152,7 @@ async function start(config) {
2682
3152
  status("Waiting for Plugin connection...");
2683
3153
  const hashTracker = createHashTracker();
2684
3154
  const fileMetadataCache = new FileMetadataCache();
3155
+ const pendingRenameConfirmations = /* @__PURE__ */ new Map();
2685
3156
  let installer = null;
2686
3157
  let syncState = {
2687
3158
  mode: "disconnected",
@@ -2703,13 +3174,24 @@ async function start(config) {
2703
3174
  hashTracker,
2704
3175
  installer,
2705
3176
  fileMetadataCache,
3177
+ pendingRenameConfirmations,
2706
3178
  userActions,
2707
3179
  syncState
2708
3180
  });
2709
3181
  for (const followUpEvent of followUpEvents) await processEvent(followUpEvent);
2710
3182
  }
2711
3183
  }
2712
- const connection = await initConnection(config.port);
3184
+ const certs = await getOrCreateCerts();
3185
+ if (!certs) {
3186
+ error("Failed to generate TLS certificates. The Framer plugin requires a secure (wss://) connection.");
3187
+ info("");
3188
+ info("To fix this:");
3189
+ info(" 1. Re-run this command — certificate generation is often a one-time issue");
3190
+ info(` 2. Manually delete "${String(CERT_DIR)}" and try again`);
3191
+ info("");
3192
+ throw new Error("TLS certificate generation failed");
3193
+ }
3194
+ const connection = await initConnection(config.port, certs);
2713
3195
  connection.on("handshake", (client, message) => {
2714
3196
  debug(`Received handshake: ${message.projectName} (${message.projectId})`);
2715
3197
  const expectedShort = shortProjectHash(config.projectHash);
@@ -2727,6 +3209,7 @@ async function start(config) {
2727
3209
  return;
2728
3210
  }
2729
3211
  debug(`New handshake received in ${syncState.mode} mode, resetting sync state`);
3212
+ pendingRenameConfirmations.clear();
2730
3213
  await processEvent({ type: "DISCONNECT" });
2731
3214
  }
2732
3215
  if (!wasRecentlyDisconnected() && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
@@ -2808,6 +3291,10 @@ async function start(config) {
2808
3291
  remoteModifiedAt: message.remoteModifiedAt
2809
3292
  };
2810
3293
  break;
3294
+ case "error":
3295
+ if (message.fileName) pendingRenameConfirmations.delete(normalizeCodeFilePathWithExtension(message.fileName));
3296
+ warn(message.message);
3297
+ return;
2811
3298
  case "conflicts-resolved":
2812
3299
  event = {
2813
3300
  type: "CONFLICTS_RESOLVED",
@@ -2844,6 +3331,7 @@ async function start(config) {
2844
3331
  status("Disconnected, waiting to reconnect...");
2845
3332
  });
2846
3333
  (async () => {
3334
+ pendingRenameConfirmations.clear();
2847
3335
  await processEvent({ type: "DISCONNECT" });
2848
3336
  userActions.cleanup();
2849
3337
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-code-link",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "CLI tool for syncing Framer code components - controller-centric architecture",
5
5
  "main": "dist/index.mjs",
6
6
  "type": "module",