framer-code-link 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +196 -345
  2. package/package.json +2 -1
package/dist/index.mjs CHANGED
@@ -5,6 +5,7 @@ import fs from "fs/promises";
5
5
  import { WebSocketServer } from "ws";
6
6
  import chokidar from "chokidar";
7
7
  import path from "path";
8
+ import { getPortFromHash, isSupportedExtension, normalizePath, pluralize, sanitizeFilePath, shortProjectHash } from "@code-link/shared";
8
9
  import { createHash } from "crypto";
9
10
  import { setupTypeAcquisition } from "@typescript/ata";
10
11
  import ts from "typescript";
@@ -118,7 +119,6 @@ let LogLevel = /* @__PURE__ */ function(LogLevel$1) {
118
119
  let currentLevel = LogLevel.INFO;
119
120
  let lastMessage = "";
120
121
  let lastMessageCount = 0;
121
- let lastCategory = "other";
122
122
  const CLEAR_LINE = "\x1B[2K";
123
123
  const MOVE_CURSOR_UP = "\x1B[1A";
124
124
  function rewriteLastLine(text) {
@@ -144,16 +144,6 @@ function flushDedupe() {
144
144
  lastMessageCount = 0;
145
145
  }
146
146
  /**
147
- * Handle category transition - adds newline when switching categories
148
- */
149
- function transitionCategory(newCategory) {
150
- if (lastCategory !== newCategory) {
151
- flushDedupe();
152
- console.log();
153
- lastCategory = newCategory;
154
- }
155
- }
156
- /**
157
147
  * Log with deduplication - repeated messages within window get counted
158
148
  */
159
149
  function logWithDedupe(message, writer) {
@@ -187,9 +177,10 @@ function debug(message, ...args) {
187
177
  */
188
178
  function info(message, ...args) {
189
179
  if (currentLevel <= LogLevel.INFO) {
190
- transitionCategory("other");
191
180
  const formatted = args.length > 0 ? `${message} ${args.join(" ")}` : message;
192
- logWithDedupe(formatted, () => console.log(formatted));
181
+ logWithDedupe(formatted, () => {
182
+ console.log(formatted);
183
+ });
193
184
  }
194
185
  }
195
186
  /**
@@ -198,7 +189,6 @@ function info(message, ...args) {
198
189
  function warn(message, ...args) {
199
190
  if (currentLevel <= LogLevel.WARN) {
200
191
  if (message === lastMessage) return;
201
- transitionCategory("other");
202
192
  flushDedupe();
203
193
  lastMessage = message;
204
194
  lastMessageCount = 1;
@@ -210,7 +200,6 @@ function warn(message, ...args) {
210
200
  */
211
201
  function error(message, ...args) {
212
202
  if (currentLevel <= LogLevel.ERROR) {
213
- transitionCategory("other");
214
203
  flushDedupe();
215
204
  console.error(import_picocolors.default.red(`✗ ${message}`), ...args);
216
205
  }
@@ -220,7 +209,6 @@ function error(message, ...args) {
220
209
  */
221
210
  function success(message, ...args) {
222
211
  if (currentLevel <= LogLevel.INFO) {
223
- transitionCategory("other");
224
212
  flushDedupe();
225
213
  console.log(import_picocolors.default.green(`✓ ${message}`), ...args);
226
214
  }
@@ -230,23 +218,26 @@ function success(message, ...args) {
230
218
  */
231
219
  function fileDown(fileName) {
232
220
  if (currentLevel <= LogLevel.INFO) {
233
- transitionCategory("file-sync");
234
221
  const msg = ` ${import_picocolors.default.blue("↓")} ${fileName}`;
235
- logWithDedupe(msg, () => console.log(msg));
222
+ logWithDedupe(msg, () => {
223
+ console.log(msg);
224
+ });
236
225
  }
237
226
  }
238
227
  function fileUp(fileName) {
239
228
  if (currentLevel <= LogLevel.INFO) {
240
- transitionCategory("file-sync");
241
229
  const msg = ` ${import_picocolors.default.green("↑")} ${fileName}`;
242
- logWithDedupe(msg, () => console.log(msg));
230
+ logWithDedupe(msg, () => {
231
+ console.log(msg);
232
+ });
243
233
  }
244
234
  }
245
235
  function fileDelete(fileName) {
246
236
  if (currentLevel <= LogLevel.INFO) {
247
- transitionCategory("file-sync");
248
237
  const msg = ` ${import_picocolors.default.red("×")} ${fileName}`;
249
- logWithDedupe(msg, () => console.log(msg));
238
+ logWithDedupe(msg, () => {
239
+ console.log(msg);
240
+ });
250
241
  }
251
242
  }
252
243
  /**
@@ -254,7 +245,6 @@ function fileDelete(fileName) {
254
245
  */
255
246
  function status(message) {
256
247
  if (currentLevel <= LogLevel.INFO) {
257
- transitionCategory("other");
258
248
  flushDedupe();
259
249
  console.log(import_picocolors.default.dim(` ${message}`));
260
250
  }
@@ -307,8 +297,7 @@ function resetDisconnectState() {
307
297
  /**
308
298
  * WebSocket connection helper
309
299
  *
310
- * Thin wrapper around ws.Server that normalizes handshake and surfaces
311
- * simple callbacks. Keeps raw socket API localized.
300
+ * Wrapper around ws.Server that normalizes handshake and surfaces callbacks.
312
301
  */
313
302
  /**
314
303
  * Initializes a WebSocket server and returns a connection interface
@@ -360,7 +349,7 @@ function initConnection(port) {
360
349
  }
361
350
  });
362
351
  ws.on("close", (code, reason) => {
363
- debug(`Client disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`);
352
+ debug(`Client disconnected (code: ${code}, reason: ${reason.toString()})`);
364
353
  handlers.onDisconnect?.();
365
354
  });
366
355
  ws.on("error", (err) => {
@@ -369,10 +358,20 @@ function initConnection(port) {
369
358
  });
370
359
  resolve({
371
360
  on(event, handler) {
372
- if (event === "handshake") handlers.onHandshake = handler;
373
- else if (event === "message") handlers.onMessage = handler;
374
- else if (event === "disconnect") handlers.onDisconnect = handler;
375
- else if (event === "error") handlers.onError = handler;
361
+ switch (event) {
362
+ case "handshake":
363
+ handlers.onHandshake = handler;
364
+ break;
365
+ case "message":
366
+ handlers.onMessage = handler;
367
+ break;
368
+ case "disconnect":
369
+ handlers.onDisconnect = handler;
370
+ break;
371
+ case "error":
372
+ handlers.onError = handler;
373
+ break;
374
+ }
376
375
  },
377
376
  close() {
378
377
  wss.close();
@@ -421,174 +420,7 @@ function sendMessage(socket, message) {
421
420
  }
422
421
 
423
422
  //#endregion
424
- //#region ../shared/dist/hash.js
425
- /**
426
- * Base58 alphabet (no 0/O/I/l to avoid confusion)
427
- */
428
- const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
429
- /**
430
- * Derive a short, deterministic hash from the full Framer project hash.
431
- * Uses a simple numeric hash encoded in base58 for compactness.
432
- * Idempotent: if input is already the target length, returns it unchanged.
433
- */
434
- function shortProjectHash(fullHash, length = 8) {
435
- if (fullHash.length === length) return fullHash;
436
- let h1 = 0;
437
- let h2 = 0;
438
- for (let i = 0; i < fullHash.length; i++) {
439
- const char = fullHash.charCodeAt(i);
440
- h1 = Math.imul(h1 ^ char, 2246822507);
441
- h2 = Math.imul(h2 ^ char, 3266489909);
442
- }
443
- h1 ^= h2 >>> 16;
444
- h2 ^= h1 >>> 13;
445
- let result = "";
446
- const combined = [Math.abs(h1), Math.abs(h2)];
447
- for (const num of combined) {
448
- let n = num >>> 0;
449
- while (n > 0 && result.length < length) {
450
- result += BASE58[n % 58];
451
- n = Math.floor(n / 58);
452
- }
453
- }
454
- while (result.length < length) result += BASE58[0];
455
- return result.slice(0, length);
456
- }
457
-
458
- //#endregion
459
- //#region ../shared/dist/ports.js
460
- /**
461
- * Generate a deterministic port number from a project hash (full or short).
462
- * Port range: 3847-4096 (250 possible ports)
463
- * Must match between CLI and plugin.
464
- *
465
- * Internally normalizes to the short id so both full and short inputs yield the same port.
466
- */
467
- function getPortFromHash(projectHash) {
468
- const shortId = shortProjectHash(projectHash);
469
- let hash = 0;
470
- for (let i = 0; i < shortId.length; i++) {
471
- const char = shortId.charCodeAt(i);
472
- hash = (hash << 5) - hash + char;
473
- hash = hash & hash;
474
- }
475
- return 3847 + Math.abs(hash) % 250;
476
- }
477
-
478
- //#endregion
479
- //#region ../shared/dist/paths.js
480
- /**
481
- * File path normalization utilities
482
- * Framer code files include extensions in their paths (.tsx, .ts, etc.)
483
- */
484
- const firstCharacterRegex = /^[a-zA-Z$_]/;
485
- const remainingCharactersRegex = /[^a-zA-Z0-9$_]/g;
486
- const onlyDotsRegex = /^\.+$/;
487
- const tsxExtension = ".tsx";
488
- var NameType;
489
- (function(NameType$1) {
490
- NameType$1["Variable"] = "Variable";
491
- NameType$1["Selector"] = "Selector";
492
- NameType$1["Directory"] = "Directory";
493
- })(NameType || (NameType = {}));
494
- function sanitizedName(type, name) {
495
- if (!name) return null;
496
- let validName = name.trim();
497
- if (validName.length === 0) return null;
498
- const validFirstChar = type === NameType.Selector ? "_" : "$";
499
- if (type === NameType.Directory) {
500
- if (onlyDotsRegex.test(validName)) return null;
501
- } else if (!firstCharacterRegex.test(validName)) validName = validFirstChar + validName;
502
- validName = validName.replace(remainingCharactersRegex, "_");
503
- validName = validName.replace(/_+/g, "_");
504
- validName = validName.replace(/^\$_/u, validFirstChar);
505
- return validName;
506
- }
507
- function sanitizedVariableName(name) {
508
- return sanitizedName(NameType.Variable, name);
509
- }
510
- function sanitizedDirectoryName(name) {
511
- return sanitizedName(NameType.Directory, name);
512
- }
513
- function capitalizeFirstLetter(str) {
514
- if (str.length === 0) return str;
515
- return str.charAt(0).toUpperCase() + str.slice(1);
516
- }
517
- function hasValidExtension(fileName) {
518
- if (fileName.endsWith(".json")) return true;
519
- return /\.[tj]sx?$/u.test(fileName);
520
- }
521
- function splitExtension(fileName) {
522
- const match = fileName.match(/^(.+?)(\.[^.]+)?$/);
523
- if (!match) return [fileName, ""];
524
- return [match[1], match[2]?.slice(1) || ""];
525
- }
526
- function dirname(filePath) {
527
- const at = filePath.lastIndexOf("/");
528
- if (at < 0) return "";
529
- return filePath.slice(0, at);
530
- }
531
- function filename(filePath) {
532
- const at = filePath.lastIndexOf("/") + 1;
533
- return filePath.slice(at);
534
- }
535
- function pathJoin(...parts) {
536
- let res = "";
537
- parts.forEach((part) => {
538
- while (part.startsWith("/")) part = part.slice(1);
539
- while (part.endsWith("/")) part = part.slice(0, -1);
540
- if (part === "") return;
541
- if (res !== "") res += "/";
542
- res += part;
543
- });
544
- return res;
545
- }
546
- function normalizePath(filePath) {
547
- if (!filePath) return "";
548
- const isAbsolute = filePath.startsWith("/");
549
- const segments = filePath.replace(/\\/g, "/").split("/");
550
- const stack = [];
551
- for (const segment of segments) {
552
- if (!segment || segment === ".") continue;
553
- if (segment === "..") {
554
- if (stack.length > 0) stack.pop();
555
- continue;
556
- }
557
- stack.push(segment);
558
- }
559
- const normalized = stack.join("/");
560
- if (isAbsolute) return `/${normalized}`;
561
- return normalized;
562
- }
563
- function sanitizeFilePath(input, capitalizeReactComponent = true) {
564
- const trimmed = input.trim();
565
- let [inputName, extension] = splitExtension(filename(trimmed));
566
- if (extension) extension = `.${extension}`;
567
- const dirName = dirname(trimmed).split("/").map((part) => sanitizedDirectoryName(part)).filter((part) => Boolean(part)).join("/");
568
- let name = sanitizedVariableName(inputName) ?? "MyComponent";
569
- if ((!hasValidExtension(extension) || extension === tsxExtension) && capitalizeReactComponent) name = capitalizeFirstLetter(name);
570
- return {
571
- path: pathJoin(dirName, name + extension),
572
- dirName,
573
- name,
574
- extension
575
- };
576
- }
577
- function isSupportedExtension$1(filePath) {
578
- return /\.(tsx?|jsx?|json)$/i.test(filePath);
579
- }
580
- /**
581
- * Pluralize a word based on count
582
- * @example pluralize(1, "file") => "1 file"
583
- * @example pluralize(3, "file") => "3 files"
584
- * @example pluralize(0, "conflict") => "0 conflicts"
585
- */
586
- function pluralize(count, singular, plural) {
587
- return `${count} ${count === 1 ? singular : plural ?? `${singular}s`}`;
588
- }
589
-
590
- //#endregion
591
- //#region src/utils/paths.ts
423
+ //#region src/utils/node-paths.ts
592
424
  /**
593
425
  * Path manipulation utilities
594
426
  */
@@ -627,9 +459,7 @@ function normalizePath$1(filePath) {
627
459
  /**
628
460
  * File watcher helper
629
461
  *
630
- * Thin wrapper around chokidar that normalizes file paths and emits
631
- * only supported file types (ts, tsx, js, json). Controller never worries
632
- * about addDir or platform separators.
462
+ * Wrapper around chokidar that normalizes file paths and filters to ts, tsx, js, json.
633
463
  */
634
464
  /**
635
465
  * Initializes a file watcher for the given directory
@@ -637,13 +467,13 @@ function normalizePath$1(filePath) {
637
467
  function initWatcher(filesDir) {
638
468
  const handlers = [];
639
469
  const watcher = chokidar.watch(filesDir, {
640
- ignored: /(^|[\/\\])\../,
470
+ ignored: /(^|[/\\])\.\./,
641
471
  persistent: true,
642
472
  ignoreInitial: false
643
473
  });
644
474
  debug(`Watching directory: ${filesDir}`);
645
475
  const emitEvent = async (kind, absolutePath) => {
646
- if (!isSupportedExtension$1(absolutePath)) return;
476
+ if (!isSupportedExtension(absolutePath)) return;
647
477
  const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
648
478
  const relativePath = sanitizeFilePath(rawRelativePath, false).path;
649
479
  let effectiveAbsolutePath = absolutePath;
@@ -673,12 +503,18 @@ function initWatcher(filesDir) {
673
503
  debug(`Watcher event: ${kind} ${relativePath}`);
674
504
  for (const handler of handlers) handler(event);
675
505
  };
676
- watcher.on("add", (filePath) => emitEvent("add", filePath));
677
- watcher.on("change", (filePath) => emitEvent("change", filePath));
678
- watcher.on("unlink", (filePath) => emitEvent("delete", filePath));
506
+ watcher.on("add", (filePath) => {
507
+ emitEvent("add", filePath);
508
+ });
509
+ watcher.on("change", (filePath) => {
510
+ emitEvent("change", filePath);
511
+ });
512
+ watcher.on("unlink", (filePath) => {
513
+ emitEvent("delete", filePath);
514
+ });
679
515
  return {
680
- on(event, handler) {
681
- if (event === "change") handlers.push(handler);
516
+ on(_event, handler) {
517
+ handlers.push(handler);
682
518
  },
683
519
  async close() {
684
520
  await watcher.close();
@@ -696,7 +532,7 @@ function initWatcher(filesDir) {
696
532
  * (hash matches), because that means the file wasn't edited while CLI was offline.
697
533
  */
698
534
  const STATE_FILE_NAME = ".framer-sync-state.json";
699
- const CURRENT_VERSION = 2;
535
+ const CURRENT_VERSION = 1;
700
536
  const SUPPORTED_EXTENSIONS$1 = [
701
537
  ".ts",
702
538
  ".tsx",
@@ -801,7 +637,7 @@ async function listFiles(filesDir) {
801
637
  await walk(entryPath);
802
638
  continue;
803
639
  }
804
- if (!isSupportedExtension(entry.name)) continue;
640
+ if (!isSupportedExtension$1(entry.name)) continue;
805
641
  const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
806
642
  try {
807
643
  const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
@@ -831,7 +667,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
831
667
  const preferRemote = options.preferRemote ?? false;
832
668
  const persistedState = options.persistedState;
833
669
  const getPersistedState = (fileName) => persistedState?.get(normalizeForComparison(fileName)) ?? persistedState?.get(fileName);
834
- debug(`Detecting conflicts for ${remoteFiles.length} remote files`);
670
+ debug(`Detecting conflicts for ${String(remoteFiles.length)} remote files`);
835
671
  const localFiles = await listFiles(filesDir);
836
672
  const localFileMap = new Map(localFiles.map((f) => [normalizeForComparison(f.name), f]));
837
673
  const remoteFileMap = new Map(remoteFiles.map((f) => {
@@ -854,7 +690,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
854
690
  localContent: null,
855
691
  remoteContent: remote.content,
856
692
  remoteModifiedAt: remote.modifiedAt,
857
- lastSyncedAt: persisted?.timestamp
693
+ lastSyncedAt: persisted.timestamp
858
694
  });
859
695
  } else writes.push({
860
696
  name: normalized.relativePath,
@@ -896,13 +732,13 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
896
732
  const persisted = getPersistedState(local.name);
897
733
  if (persisted) {
898
734
  const localClean = hashFileContent(local.content) === persisted.contentHash;
899
- debug(`Conflict: ${local.name} deleted in Framer (localClean=${localClean})`);
735
+ debug(`Conflict: ${local.name} deleted in Framer (localClean=${String(localClean)})`);
900
736
  conflicts.push({
901
737
  fileName: local.name,
902
738
  localContent: local.content,
903
739
  remoteContent: null,
904
740
  localModifiedAt: local.modifiedAt,
905
- lastSyncedAt: persisted?.timestamp,
741
+ lastSyncedAt: persisted.timestamp,
906
742
  localClean
907
743
  });
908
744
  } else localOnly.push({
@@ -1006,7 +842,7 @@ async function deleteLocalFile(fileName, filesDir, hashTracker) {
1006
842
  hashTracker.forget(normalized.relativePath);
1007
843
  debug(`Deleted file: ${normalized.relativePath}`);
1008
844
  } catch (err) {
1009
- if (err?.code === "ENOENT") {
845
+ if (err.code === "ENOENT") {
1010
846
  hashTracker.forget(normalized.relativePath);
1011
847
  debug(`File already deleted: ${normalized.relativePath}`);
1012
848
  return;
@@ -1026,6 +862,15 @@ async function readFileSafe(fileName, filesDir) {
1026
862
  return null;
1027
863
  }
1028
864
  }
865
+ /**
866
+ * Filter out files whose content matches the last remembered hash.
867
+ * Used to skip inbound echoes of our own local sends.
868
+ */
869
+ function filterEchoedFiles(files, hashTracker) {
870
+ return files.filter((file) => {
871
+ return !hashTracker.shouldSkip(file.name, file.content);
872
+ });
873
+ }
1029
874
  function resolveRemoteReference(filesDir, rawName) {
1030
875
  const normalized = sanitizeRelativePath(rawName);
1031
876
  const absolutePath = path.join(filesDir, normalized.relativePath);
@@ -1043,7 +888,7 @@ function sanitizeRelativePath(relativePath) {
1043
888
  extension: sanitized.extension || path.extname(normalized) || DEFAULT_EXTENSION
1044
889
  };
1045
890
  }
1046
- function isSupportedExtension(fileName) {
891
+ function isSupportedExtension$1(fileName) {
1047
892
  const lower = fileName.toLowerCase();
1048
893
  return SUPPORTED_EXTENSIONS.some((ext) => lower.endsWith(ext));
1049
894
  }
@@ -1056,7 +901,7 @@ function isSupportedExtension(fileName) {
1056
901
  function extractImports(code) {
1057
902
  const imports = [];
1058
903
  const seen = /* @__PURE__ */ new Set();
1059
- const npmRegex = /import\s+(?:(?:\*\s+as\s+\w+)|(?:\w+)|(?:\{[^}]*\}))\s+from\s+['"]([^.\/][^'"]+)['"]/g;
904
+ const npmRegex = /import\s+(?:(?:\*\s+as\s+\w+)|(?:\w+)|(?:\{[^}]*\}))\s+from\s+['"]([^./][^'"]+)['"]/g;
1060
905
  const urlRegex = /import\s+(?:(?:\*\s+as\s+\w+)|(?:\w+)|(?:\{[^}]*\}))\s+from\s+['"]https?:\/\/[^'"]+['"]/g;
1061
906
  let match;
1062
907
  while ((match = npmRegex.exec(code)) !== null) {
@@ -1088,7 +933,7 @@ function extractImports(code) {
1088
933
  * Attempt to derive an npm-style package specifier from a URL import.
1089
934
  */
1090
935
  function extractPackageFromUrl(url) {
1091
- return url.match(/\/(@?[^@\/]+(?:\/[^@\/]+)?)/)?.[1] ?? null;
936
+ return /\/(@?[^@/]+(?:\/[^@/]+)?)/.exec(url)?.[1] ?? null;
1092
937
  }
1093
938
 
1094
939
  //#endregion
@@ -1141,29 +986,31 @@ var Installer = class {
1141
986
  },
1142
987
  progress: () => {},
1143
988
  finished: (files) => {
1144
- if (files && files.size > 0) debug("ATA: type acquisition complete");
989
+ if (files.size > 0) debug("ATA: type acquisition complete");
1145
990
  },
1146
991
  errorMessage: (message, error$1) => {
1147
992
  warn(`ATA warning: ${message}`, error$1);
1148
993
  },
1149
- receivedFile: async (code, receivedPath) => {
1150
- const normalized = receivedPath.replace(/^\//, "");
1151
- const destination = path.join(this.projectDir, normalized);
1152
- const pkgMatch = receivedPath.match(/\/node_modules\/(@?[^\/]+(?:\/[^\/]+)?)\//);
1153
- try {
1154
- if (await fs.readFile(destination, "utf-8") === code) {
1155
- if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
1156
- seenPackages.add(pkgMatch[1]);
1157
- debug(`📦 Types: ${pkgMatch[1]} (from disk cache)`);
994
+ receivedFile: (code, receivedPath) => {
995
+ (async () => {
996
+ const normalized = receivedPath.replace(/^\//, "");
997
+ const destination = path.join(this.projectDir, normalized);
998
+ const pkgMatch = /\/node_modules\/(@?[^/]+(?:\/[^/]+)?)\//.exec(receivedPath);
999
+ try {
1000
+ if (await fs.readFile(destination, "utf-8") === code) {
1001
+ if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
1002
+ seenPackages.add(pkgMatch[1]);
1003
+ debug(`📦 Types: ${pkgMatch[1]} (from disk cache)`);
1004
+ }
1005
+ return;
1158
1006
  }
1159
- return;
1007
+ } catch {}
1008
+ if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
1009
+ seenPackages.add(pkgMatch[1]);
1010
+ debug(`📦 Types: ${pkgMatch[1]}`);
1160
1011
  }
1161
- } catch {}
1162
- if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
1163
- seenPackages.add(pkgMatch[1]);
1164
- debug(`📦 Types: ${pkgMatch[1]}`);
1165
- }
1166
- await this.writeTypeFile(receivedPath, code);
1012
+ await this.writeTypeFile(receivedPath, code);
1013
+ })();
1167
1014
  }
1168
1015
  }
1169
1016
  });
@@ -1210,10 +1057,10 @@ var Installer = class {
1210
1057
  });
1211
1058
  }
1212
1059
  async processImports(fileName, content) {
1213
- const allImports = extractImports(content).filter((imp) => imp.type === "npm");
1060
+ const allImports = extractImports(content).filter((i) => i.type === "npm");
1214
1061
  if (allImports.length === 0) return;
1215
- const imports = this.allowUnsupportedNpm ? allImports : allImports.filter((imp) => this.isSupportedPackage(imp.name));
1216
- if (allImports.length - imports.length > 0 && !this.allowUnsupportedNpm) debug(`Skipping unsupported packages: ${allImports.filter((imp) => !this.isSupportedPackage(imp.name)).map((imp) => imp.name).join(", ")} (use --unsupported-npm to enable)`);
1062
+ const imports = this.allowUnsupportedNpm ? allImports : allImports.filter((i) => this.isSupportedPackage(i.name));
1063
+ if (allImports.length - imports.length > 0 && !this.allowUnsupportedNpm) debug(`Skipping unsupported packages: ${allImports.filter((i) => !this.isSupportedPackage(i.name)).map((i) => i.name).join(", ")} (use --unsupported-npm to enable)`);
1217
1064
  if (imports.length === 0) return;
1218
1065
  const hash = imports.map((imp) => imp.name).sort().join(",");
1219
1066
  if (this.processedImports.has(hash)) return;
@@ -1251,11 +1098,11 @@ var Installer = class {
1251
1098
  warn(`Failed to write type file ${destination}`, err);
1252
1099
  return;
1253
1100
  }
1254
- if (normalized.match(/node_modules\/@types\/[^\/]+\/index\.d\.ts$/)) await this.ensureTypesPackageJson(normalized);
1101
+ if (/node_modules\/@types\/[^/]+\/index\.d\.ts$/.exec(normalized)) await this.ensureTypesPackageJson(normalized);
1255
1102
  if (normalized.includes("node_modules/@types/react/index.d.ts")) await this.patchReactTypes(destination);
1256
1103
  }
1257
1104
  async ensureTypesPackageJson(normalizedPath) {
1258
- const pkgMatch = normalizedPath.match(/node_modules\/(@types\/[^\/]+)\//);
1105
+ const pkgMatch = /node_modules\/(@types\/[^/]+)\//.exec(normalizedPath);
1259
1106
  if (!pkgMatch) return;
1260
1107
  const pkgName = pkgMatch[1];
1261
1108
  const pkgDir = path.join(this.projectDir, "node_modules", pkgName);
@@ -1267,16 +1114,7 @@ var Installer = class {
1267
1114
  const version$1 = npmData["dist-tags"]?.latest;
1268
1115
  if (!version$1 || !npmData.versions?.[version$1]) return;
1269
1116
  const pkg = npmData.versions[version$1];
1270
- if (pkg.exports && typeof pkg.exports === "object") {
1271
- const fixExport = (value) => {
1272
- if (typeof value === "string") return { types: value.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts") };
1273
- if (value && typeof value === "object") {
1274
- if ((value.import || value.require) && !value.types) value.types = (value.import || value.require).replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts");
1275
- }
1276
- return value;
1277
- };
1278
- for (const key of Object.keys(pkg.exports)) pkg.exports[key] = fixExport(pkg.exports[key]);
1279
- }
1117
+ if (pkg.exports) for (const key of Object.keys(pkg.exports)) pkg.exports[key] = fixExportTypes(pkg.exports[key]);
1280
1118
  await fs.mkdir(pkgDir, { recursive: true });
1281
1119
  await fs.writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2));
1282
1120
  } catch {}
@@ -1286,7 +1124,7 @@ var Installer = class {
1286
1124
  let content = await fs.readFile(destination, "utf-8");
1287
1125
  if (content.includes("function useRef<T = undefined>()")) return;
1288
1126
  const overloadPattern = /function useRef<T>\(initialValue: T \| undefined\): RefObject<T \| undefined>;/;
1289
- if (!overloadPattern.test(content)) return;
1127
+ if (!content.includes("function useRef<T>(initialValue: T | undefined)")) return;
1290
1128
  content = content.replace(overloadPattern, `function useRef<T>(initialValue: T | undefined): RefObject<T | undefined>;
1291
1129
  function useRef<T = undefined>(): MutableRefObject<T | undefined>;`);
1292
1130
  await fs.writeFile(destination, content, "utf-8");
@@ -1426,11 +1264,24 @@ declare module "*.json"
1426
1264
  }));
1427
1265
  }
1428
1266
  };
1267
+ /**
1268
+ * Transform package.json exports to include .d.ts type paths
1269
+ */
1270
+ function fixExportTypes(value) {
1271
+ if (typeof value === "string") return { types: value.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts") };
1272
+ if ((value.import ?? value.require) && !value.types) value.types = (value.import ?? value.require)?.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts");
1273
+ return value;
1274
+ }
1429
1275
  async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
1430
- const urlString = typeof url === "string" ? url : url.toString();
1276
+ let urlString;
1277
+ if (typeof url === "string") urlString = url;
1278
+ else if (url instanceof URL) urlString = url.href;
1279
+ else urlString = url.url;
1431
1280
  for (let attempt = 1; attempt <= retries; attempt++) {
1432
1281
  const controller = new AbortController();
1433
- const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1282
+ const timeout = setTimeout(() => {
1283
+ controller.abort();
1284
+ }, FETCH_TIMEOUT_MS);
1434
1285
  try {
1435
1286
  const response = await fetch(url, {
1436
1287
  ...init,
@@ -1438,12 +1289,13 @@ async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
1438
1289
  });
1439
1290
  clearTimeout(timeout);
1440
1291
  return response;
1441
- } catch (error$1) {
1292
+ } catch (err) {
1442
1293
  clearTimeout(timeout);
1443
- const isRetryable = error$1?.cause?.code === "ECONNRESET" || error$1?.cause?.code === "ETIMEDOUT" || error$1?.cause?.code === "UND_ERR_CONNECT_TIMEOUT" || error$1?.message?.includes("timeout");
1294
+ const error$1 = err;
1295
+ const isRetryable = error$1.cause?.code === "ECONNRESET" || error$1.cause?.code === "ETIMEDOUT" || error$1.cause?.code === "UND_ERR_CONNECT_TIMEOUT" || error$1.message.includes("timeout");
1444
1296
  if (attempt < retries && isRetryable) {
1445
1297
  const delay = attempt * 1e3;
1446
- warn(`Fetch failed (${error$1?.cause?.code || error$1?.message}) for ${urlString}, retrying in ${delay}ms...`);
1298
+ warn(`Fetch failed (${error$1.cause?.code ?? error$1.message}) for ${urlString}, retrying in ${delay}ms...`);
1447
1299
  await new Promise((resolve) => setTimeout(resolve, delay));
1448
1300
  continue;
1449
1301
  }
@@ -1575,11 +1427,12 @@ var FileMetadataCache = class {
1575
1427
  if (this.pendingPersist) await this.pendingPersist;
1576
1428
  }
1577
1429
  schedulePersist() {
1578
- if (!this.projectDir) return;
1579
- if (!this.pendingPersist) this.pendingPersist = (async () => {
1430
+ const projectDir = this.projectDir;
1431
+ if (!projectDir) return;
1432
+ this.pendingPersist ??= (async () => {
1580
1433
  try {
1581
1434
  await Promise.resolve();
1582
- await savePersistedState(this.projectDir, this.persisted);
1435
+ await savePersistedState(projectDir, this.persisted);
1583
1436
  } finally {
1584
1437
  this.pendingPersist = null;
1585
1438
  }
@@ -1598,12 +1451,24 @@ var PluginDisconnectedError = class extends Error {
1598
1451
  var UserActionCoordinator = class {
1599
1452
  pendingActions = /* @__PURE__ */ new Map();
1600
1453
  /**
1454
+ * Register a pending action and return a typed promise
1455
+ */
1456
+ awaitAction(actionId, description) {
1457
+ return new Promise((resolve, reject) => {
1458
+ this.pendingActions.set(actionId, {
1459
+ resolve,
1460
+ reject
1461
+ });
1462
+ debug(`Awaiting ${description}: ${actionId}`);
1463
+ });
1464
+ }
1465
+ /**
1601
1466
  * Sends the delete request to the plugin and awaits the user's decision
1602
1467
  */
1603
1468
  async requestDeleteDecision(socket, { fileName, requireConfirmation }) {
1604
1469
  if (!socket) throw new Error("Cannot request delete decision: plugin not connected");
1605
1470
  if (requireConfirmation) {
1606
- const confirmationPromise = this.awaitConfirmation(`delete:${fileName}`, "delete confirmation");
1471
+ const confirmationPromise = this.awaitAction(`delete:${fileName}`, "delete confirmation");
1607
1472
  await sendMessage(socket, {
1608
1473
  type: "file-delete",
1609
1474
  fileNames: [fileName],
@@ -1634,7 +1499,7 @@ var UserActionCoordinator = class {
1634
1499
  if (conflicts.length === 0) return /* @__PURE__ */ new Map();
1635
1500
  const pending = conflicts.map((conflict) => ({
1636
1501
  fileName: conflict.fileName,
1637
- promise: this.awaitConfirmation(`conflict:${conflict.fileName}`, "conflict resolution")
1502
+ promise: this.awaitAction(`conflict:${conflict.fileName}`, "conflict resolution")
1638
1503
  }));
1639
1504
  await sendMessage(socket, {
1640
1505
  type: "conflicts-detected",
@@ -1652,18 +1517,6 @@ var UserActionCoordinator = class {
1652
1517
  }
1653
1518
  }
1654
1519
  /**
1655
- * Generic confirmation awaiter
1656
- */
1657
- awaitConfirmation(actionId, description) {
1658
- return new Promise((resolve, reject) => {
1659
- this.pendingActions.set(actionId, {
1660
- resolve,
1661
- reject
1662
- });
1663
- debug(`Awaiting ${description}: ${actionId}`);
1664
- });
1665
- }
1666
- /**
1667
1520
  * Handle incoming confirmation response
1668
1521
  */
1669
1522
  handleConfirmation(actionId, value) {
@@ -1700,7 +1553,7 @@ var UserActionCoordinator = class {
1700
1553
  * Note: This is for INCOMING changes from remote. Local changes (from watcher)
1701
1554
  * are handled separately and always sent during watching mode.
1702
1555
  */
1703
- function validateIncomingChange(file, fileMeta, currentMode) {
1556
+ function validateIncomingChange(fileMeta, currentMode) {
1704
1557
  if (currentMode === "snapshot_processing" || currentMode === "handshaking") return {
1705
1558
  action: "queue",
1706
1559
  reason: "snapshot-in-progress"
@@ -1751,7 +1604,7 @@ async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
1751
1604
  const cwd = process.cwd();
1752
1605
  const existing = await findExistingProjectDir(cwd, projectHash);
1753
1606
  if (existing) return existing;
1754
- if (!projectName) throw new Error("Project name is required when creating a new workspace. Pass --name <project name>.");
1607
+ if (!projectName) throw new Error("Failed to get Project name. Pass --name <project name>.");
1755
1608
  const dirName = toDirName(projectName);
1756
1609
  const pkgName = toPackageName(projectName);
1757
1610
  const shortId = shortProjectHash(projectHash);
@@ -1792,9 +1645,10 @@ async function matchesProject(packageJsonPath, projectHash) {
1792
1645
  //#endregion
1793
1646
  //#region src/controller.ts
1794
1647
  /**
1795
- * Controller
1796
- * Single source of truth for all runtime state and orchestrates the sync lifecycle.
1797
- * Helpers are functions that provide data - they never hold control or callbacks.
1648
+ * CLI Controller
1649
+ *
1650
+ * All runtime state and orchestrates the sync lifecycle.
1651
+ * Helpers should provide data, nevering hold control or callbacks.
1798
1652
  */
1799
1653
  /** Log helper */
1800
1654
  function log(level, message) {
@@ -1805,16 +1659,6 @@ function log(level, message) {
1805
1659
  };
1806
1660
  }
1807
1661
  /**
1808
- * Filter out files whose content matches the last remembered hash.
1809
- * Used to skip inbound echoes of our own local sends.
1810
- */
1811
- function filterEchoedFiles(files, hashTracker) {
1812
- return files.filter((file) => {
1813
- if (file.content === void 0) return true;
1814
- return !hashTracker.shouldSkip(file.name, file.content);
1815
- });
1816
- }
1817
- /**
1818
1662
  * Pure state transition function
1819
1663
  * Takes current state + event, returns new state + effects to execute
1820
1664
  */
@@ -1844,7 +1688,7 @@ function transition(state, event) {
1844
1688
  },
1845
1689
  effects
1846
1690
  };
1847
- case "FILE_SYNCED":
1691
+ case "FILE_SYNCED_CONFIRMATION":
1848
1692
  effects.push(log("debug", `Remote confirmed sync: ${event.fileName}`), {
1849
1693
  type: "UPDATE_FILE_METADATA",
1850
1694
  fileName: event.fileName,
@@ -1905,7 +1749,7 @@ function transition(state, event) {
1905
1749
  state: {
1906
1750
  ...state,
1907
1751
  mode: "snapshot_processing",
1908
- queuedDiffs: event.files
1752
+ pendingRemoteChanges: event.files
1909
1753
  },
1910
1754
  effects
1911
1755
  };
@@ -1948,7 +1792,7 @@ function transition(state, event) {
1948
1792
  effects
1949
1793
  };
1950
1794
  }
1951
- const remoteTotal = state.queuedDiffs.length;
1795
+ const remoteTotal = state.pendingRemoteChanges.length;
1952
1796
  const totalCount = remoteTotal + localOnly.length;
1953
1797
  const updatedCount = safeWrites.length + localOnly.length;
1954
1798
  const unchangedCount = Math.max(0, remoteTotal - safeWrites.length);
@@ -1962,19 +1806,19 @@ function transition(state, event) {
1962
1806
  state: {
1963
1807
  ...state,
1964
1808
  mode: "watching",
1965
- queuedDiffs: []
1809
+ pendingRemoteChanges: []
1966
1810
  },
1967
1811
  effects
1968
1812
  };
1969
1813
  }
1970
1814
  case "FILE_CHANGE": {
1971
- const validation = validateIncomingChange(event.file, event.fileMeta, state.mode);
1815
+ const validation = validateIncomingChange(event.fileMeta, state.mode);
1972
1816
  if (validation.action === "queue") {
1973
1817
  effects.push(log("debug", `Queueing file change: ${event.file.name} (${validation.reason})`));
1974
1818
  return {
1975
1819
  state: {
1976
1820
  ...state,
1977
- queuedDiffs: [...state.queuedDiffs, event.file]
1821
+ pendingRemoteChanges: [...state.pendingRemoteChanges, event.file]
1978
1822
  },
1979
1823
  effects
1980
1824
  };
@@ -2012,7 +1856,7 @@ function transition(state, event) {
2012
1856
  state,
2013
1857
  effects
2014
1858
  };
2015
- case "REMOTE_DELETE_CONFIRMED":
1859
+ case "LOCAL_DELETE_APPROVED":
2016
1860
  effects.push(log("debug", `Delete confirmed: ${event.fileName}`), {
2017
1861
  type: "DELETE_LOCAL_FILES",
2018
1862
  names: [event.fileName]
@@ -2021,7 +1865,7 @@ function transition(state, event) {
2021
1865
  state,
2022
1866
  effects
2023
1867
  };
2024
- case "REMOTE_DELETE_CANCELLED":
1868
+ case "LOCAL_DELETE_REJECTED":
2025
1869
  effects.push(log("debug", `Delete cancelled: ${event.fileName}`));
2026
1870
  effects.push({
2027
1871
  type: "WRITE_FILES",
@@ -2194,7 +2038,7 @@ function transition(state, event) {
2194
2038
  state: {
2195
2039
  ...rest,
2196
2040
  mode: "watching",
2197
- queuedDiffs: []
2041
+ pendingRemoteChanges: []
2198
2042
  },
2199
2043
  effects
2200
2044
  };
@@ -2376,14 +2220,16 @@ async function executeEffect(effect, context) {
2376
2220
  status("Watching for changes...");
2377
2221
  return [];
2378
2222
  }
2379
- case "LOG":
2380
- ({
2223
+ case "LOG": {
2224
+ const logFn = {
2381
2225
  info,
2382
2226
  warn,
2383
2227
  success,
2384
2228
  debug
2385
- }[effect.level] ?? debug)(effect.message);
2229
+ }[effect.level];
2230
+ logFn(effect.message);
2386
2231
  return [];
2232
+ }
2387
2233
  }
2388
2234
  }
2389
2235
  /**
@@ -2397,7 +2243,7 @@ async function start(config) {
2397
2243
  let syncState = {
2398
2244
  mode: "disconnected",
2399
2245
  socket: null,
2400
- queuedDiffs: [],
2246
+ pendingRemoteChanges: [],
2401
2247
  pendingOperations: /* @__PURE__ */ new Map(),
2402
2248
  nextOperationId: 1
2403
2249
  };
@@ -2423,7 +2269,7 @@ async function start(config) {
2423
2269
  }
2424
2270
  }
2425
2271
  const connection = await initConnection(config.port);
2426
- connection.on("handshake", async (client, message) => {
2272
+ connection.on("handshake", (client, message) => {
2427
2273
  debug(`Received handshake: ${message.projectName} (${message.projectId})`);
2428
2274
  const expectedShort = shortProjectHash(config.projectHash);
2429
2275
  const receivedShort = shortProjectHash(message.projectId);
@@ -2432,24 +2278,26 @@ async function start(config) {
2432
2278
  client.close();
2433
2279
  return;
2434
2280
  }
2435
- await processEvent({
2436
- type: "HANDSHAKE",
2437
- socket: client,
2438
- projectInfo: {
2439
- projectId: message.projectId,
2440
- projectName: message.projectName
2441
- }
2442
- });
2443
- if (config.projectDir && !installer) {
2444
- installer = new Installer({
2445
- projectDir: config.projectDir,
2446
- allowUnsupportedNpm: config.allowUnsupportedNpm
2281
+ (async () => {
2282
+ await processEvent({
2283
+ type: "HANDSHAKE",
2284
+ socket: client,
2285
+ projectInfo: {
2286
+ projectId: message.projectId,
2287
+ projectName: message.projectName
2288
+ }
2447
2289
  });
2448
- await installer.initialize();
2449
- startWatcher();
2450
- }
2451
- cancelDisconnectMessage();
2452
- if (!wasRecentlyDisconnected() && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
2290
+ if (config.projectDir && !installer) {
2291
+ installer = new Installer({
2292
+ projectDir: config.projectDir,
2293
+ allowUnsupportedNpm: config.allowUnsupportedNpm
2294
+ });
2295
+ await installer.initialize();
2296
+ startWatcher();
2297
+ }
2298
+ cancelDisconnectMessage();
2299
+ if (!wasRecentlyDisconnected() && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
2300
+ })();
2453
2301
  });
2454
2302
  async function handleMessage(message) {
2455
2303
  if (!config.projectDir || !installer) {
@@ -2461,15 +2309,13 @@ async function start(config) {
2461
2309
  case "request-files":
2462
2310
  event = { type: "REQUEST_FILES" };
2463
2311
  break;
2464
- case "file-list": {
2465
- const totalSize = message.files.reduce((sum, f) => sum + (f.content?.length ?? 0), 0);
2466
- debug(`Received file list: ${message.files.length} files (${(totalSize / 1024).toFixed(1)}KB)`);
2312
+ case "file-list":
2313
+ debug(`Received file list: ${message.files.length} files`);
2467
2314
  event = {
2468
2315
  type: "FILE_LIST",
2469
2316
  files: message.files
2470
2317
  };
2471
2318
  break;
2472
- }
2473
2319
  case "file-change":
2474
2320
  event = {
2475
2321
  type: "FILE_CHANGE",
@@ -2491,7 +2337,7 @@ async function start(config) {
2491
2337
  const unmatched = [];
2492
2338
  for (const fileName of message.fileNames) if (!userActions.handleConfirmation(`delete:${fileName}`, true)) unmatched.push(fileName);
2493
2339
  for (const fileName of unmatched) await processEvent({
2494
- type: "REMOTE_DELETE_CONFIRMED",
2340
+ type: "LOCAL_DELETE_APPROVED",
2495
2341
  fileName
2496
2342
  });
2497
2343
  return;
@@ -2500,7 +2346,7 @@ async function start(config) {
2500
2346
  for (const file of message.files) {
2501
2347
  userActions.handleConfirmation(`delete:${file.fileName}`, false);
2502
2348
  await processEvent({
2503
- type: "REMOTE_DELETE_CANCELLED",
2349
+ type: "LOCAL_DELETE_REJECTED",
2504
2350
  fileName: file.fileName,
2505
2351
  content: file.content ?? ""
2506
2352
  });
@@ -2508,7 +2354,7 @@ async function start(config) {
2508
2354
  return;
2509
2355
  case "file-synced":
2510
2356
  event = {
2511
- type: "FILE_SYNCED",
2357
+ type: "FILE_SYNCED_CONFIRMATION",
2512
2358
  fileName: message.fileName,
2513
2359
  remoteModifiedAt: message.remoteModifiedAt
2514
2360
  };
@@ -2529,21 +2375,25 @@ async function start(config) {
2529
2375
  warn(`Unhandled message type: ${message.type}`);
2530
2376
  return;
2531
2377
  }
2532
- if (event) await processEvent(event);
2378
+ await processEvent(event);
2533
2379
  }
2534
- connection.on("message", async (message) => {
2535
- try {
2536
- await handleMessage(message);
2537
- } catch (err) {
2538
- error("Error handling message:", err);
2539
- }
2380
+ connection.on("message", (message) => {
2381
+ (async () => {
2382
+ try {
2383
+ await handleMessage(message);
2384
+ } catch (err) {
2385
+ error("Error handling message:", err);
2386
+ }
2387
+ })();
2540
2388
  });
2541
- connection.on("disconnect", async () => {
2389
+ connection.on("disconnect", () => {
2542
2390
  scheduleDisconnectMessage(() => {
2543
2391
  status("Disconnected, waiting to reconnect...");
2544
2392
  });
2545
- await processEvent({ type: "DISCONNECT" });
2546
- userActions.cleanup();
2393
+ (async () => {
2394
+ await processEvent({ type: "DISCONNECT" });
2395
+ userActions.cleanup();
2396
+ })();
2547
2397
  });
2548
2398
  connection.on("error", (err) => {
2549
2399
  error("Error on WebSocket connection:", err);
@@ -2552,19 +2402,21 @@ async function start(config) {
2552
2402
  const startWatcher = () => {
2553
2403
  if (!config.filesDir || watcher) return;
2554
2404
  watcher = initWatcher(config.filesDir);
2555
- watcher.on("change", async (event) => {
2556
- await processEvent({
2405
+ watcher.on("change", (event) => {
2406
+ processEvent({
2557
2407
  type: "WATCHER_EVENT",
2558
2408
  event
2559
2409
  });
2560
2410
  });
2561
2411
  };
2562
- process.on("SIGINT", async () => {
2412
+ process.on("SIGINT", () => {
2563
2413
  console.log();
2564
2414
  status("Shutting down...");
2565
- if (watcher) await watcher.close();
2566
- connection.close();
2567
- process.exit(0);
2415
+ (async () => {
2416
+ if (watcher) await watcher.close();
2417
+ connection.close();
2418
+ process.exit(0);
2419
+ })();
2568
2420
  });
2569
2421
  }
2570
2422
 
@@ -2581,7 +2433,7 @@ const program = new Command();
2581
2433
  program.exitOverride((err) => {
2582
2434
  if (err.code === "commander.missingArgument") {
2583
2435
  console.error("Missing Project ID. Copy command via Code Link Plugin.");
2584
- process.exit(err.exitCode ?? 1);
2436
+ process.exit(err.exitCode);
2585
2437
  }
2586
2438
  throw err;
2587
2439
  });
@@ -2595,7 +2447,6 @@ program.name("framer-code-link").description("Sync Framer code components to you
2595
2447
  process.exit(1);
2596
2448
  }
2597
2449
  }
2598
- const isDev = process.env.NODE_ENV === "development";
2599
2450
  if (options.logLevel) {
2600
2451
  const level = {
2601
2452
  debug: LogLevel.DEBUG,
@@ -2604,7 +2455,7 @@ program.name("framer-code-link").description("Sync Framer code components to you
2604
2455
  error: LogLevel.ERROR
2605
2456
  }[options.logLevel.toLowerCase()];
2606
2457
  if (level !== void 0) setLogLevel(level);
2607
- } else if (options.verbose || isDev) setLogLevel(LogLevel.DEBUG);
2458
+ } else if (options.verbose) setLogLevel(LogLevel.DEBUG);
2608
2459
  const port = getPortFromHash(projectHash);
2609
2460
  banner(version, port);
2610
2461
  const config = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-code-link",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "CLI tool for syncing Framer code components - controller-centric architecture",
5
5
  "main": "dist/index.mjs",
6
6
  "type": "module",
@@ -22,6 +22,7 @@
22
22
  "author": "",
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
+ "@code-link/shared": "1.0.0",
25
26
  "@typescript/ata": "^0.9.8",
26
27
  "chokidar": "^5.0.0",
27
28
  "commander": "^14.0.2",