framer-code-link 0.1.3 → 0.2.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.
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { WebSocketServer, WebSocket } from "ws"
9
9
  import type { IncomingMessage, OutgoingMessage } from "../types.js"
10
- import { info, error } from "../utils/logging.js"
10
+ import { debug, error } from "../utils/logging.js"
11
11
 
12
12
  export interface ConnectionCallbacks {
13
13
  onHandshake: (
@@ -68,11 +68,12 @@ export function initConnection(port: number): Promise<Connection> {
68
68
  // Server is ready when it starts listening
69
69
  wss.on("listening", () => {
70
70
  isReady = true
71
- info(`WebSocket server listening on port ${port}`)
71
+ debug(`WebSocket server listening on port ${port}`)
72
72
 
73
73
  wss.on("connection", (ws: WebSocket) => {
74
74
  const connId = ++connectionId
75
- info(`[CONN ${connId}] Client connected (readyState: ${ws.readyState})`)
75
+ let handshakeReceived = false
76
+ debug(`Client connected (conn ${connId})`)
76
77
 
77
78
  ws.on("message", (data: Buffer) => {
78
79
  try {
@@ -80,25 +81,31 @@ export function initConnection(port: number): Promise<Connection> {
80
81
 
81
82
  // Special handling for handshake
82
83
  if (message.type === "handshake") {
83
- info(`[CONN ${connId}] Received handshake`)
84
+ debug(`Received handshake (conn ${connId})`)
85
+ handshakeReceived = true
84
86
  handlers.onHandshake?.(ws, message)
85
- } else {
87
+ } else if (handshakeReceived) {
86
88
  handlers.onMessage?.(message)
89
+ } else {
90
+ // Ignore messages before handshake - plugin will send full snapshot after
91
+ debug(
92
+ `Ignoring ${message.type} before handshake (conn ${connId})`
93
+ )
87
94
  }
88
95
  } catch (err) {
89
- error(`[CONN ${connId}] Failed to parse message:`, err)
96
+ error(`Failed to parse message:`, err)
90
97
  }
91
98
  })
92
99
 
93
100
  ws.on("close", (code, reason) => {
94
- info(
95
- `[CONN ${connId}] Client disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`
101
+ debug(
102
+ `Client disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`
96
103
  )
97
104
  handlers.onDisconnect?.()
98
105
  })
99
106
 
100
107
  ws.on("error", (err) => {
101
- error(`[CONN ${connId}] WebSocket error:`, err)
108
+ error(`WebSocket error:`, err)
102
109
  })
103
110
  })
104
111
 
@@ -163,14 +170,14 @@ export function sendMessage(
163
170
  // Check socket state before attempting to send
164
171
  if (socket.readyState !== READY_STATE.OPEN) {
165
172
  const stateStr = readyStateToString(socket.readyState)
166
- info(`[WS] Cannot send ${message.type}: socket is ${stateStr}`)
173
+ debug(`Cannot send ${message.type}: socket is ${stateStr}`)
167
174
  resolve(false)
168
175
  return
169
176
  }
170
177
 
171
178
  socket.send(JSON.stringify(message), (err) => {
172
179
  if (err) {
173
- error(`[WS] Send error for ${message.type}:`, err.message)
180
+ debug(`Send error for ${message.type}: ${err.message}`)
174
181
  resolve(false)
175
182
  } else {
176
183
  resolve(true)
@@ -18,9 +18,9 @@ import type {
18
18
  Conflict,
19
19
  ConflictVersionData,
20
20
  } from "../types.js"
21
- import type { HashTracker } from "../utils/hashing.js"
21
+ import type { HashTracker } from "../utils/hash-tracker.js"
22
22
  import { normalizePath, sanitizeFilePath } from "@code-link/shared"
23
- import { info, warn, debug } from "../utils/logging.js"
23
+ import { warn, debug } from "../utils/logging.js"
24
24
  import {
25
25
  hashFileContent,
26
26
  type PersistedFileState,
@@ -30,6 +30,11 @@ const SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".json"]
30
30
  const DEFAULT_EXTENSION = ".tsx"
31
31
  const DEFAULT_REMOTE_DRIFT_MS = 2000
32
32
 
33
+ /** Normalize file name for case-insensitive comparison (macOS/Windows compat) */
34
+ function normalizeForComparison(fileName: string): string {
35
+ return fileName.toLowerCase()
36
+ }
37
+
33
38
  /**
34
39
  * Lists all supported files in the files directory
35
40
  */
@@ -98,42 +103,79 @@ export async function detectConflicts(
98
103
  const conflicts: Conflict[] = []
99
104
  const writes: FileInfo[] = []
100
105
  const localOnly: FileInfo[] = []
106
+ const unchanged: FileInfo[] = []
101
107
  const detect = options.detectConflicts ?? true
102
108
  const preferRemote = options.preferRemote ?? false
103
109
  const persistedState = options.persistedState
104
110
 
111
+ const getPersistedState = (fileName: string) =>
112
+ persistedState?.get(normalizeForComparison(fileName)) ??
113
+ persistedState?.get(fileName)
114
+
105
115
  debug(`Detecting conflicts for ${remoteFiles.length} remote files`)
106
116
 
107
- // Build a snapshot of all local files
117
+ // Build a snapshot of all local files (keyed by lowercase for case-insensitive matching)
108
118
  const localFiles = await listFiles(filesDir)
109
- const localFileMap = new Map(localFiles.map((f) => [f.name, f]))
119
+ const localFileMap = new Map(
120
+ localFiles.map((f) => [normalizeForComparison(f.name), f])
121
+ )
110
122
 
111
- // Track which files we've processed
123
+ // Build a set of remote file names for quick lookup (lowercase keys)
124
+ const remoteFileMap = new Map(
125
+ remoteFiles.map((f) => {
126
+ const normalized = resolveRemoteReference(filesDir, f.name)
127
+ return [normalizeForComparison(normalized.relativePath), f]
128
+ })
129
+ )
130
+
131
+ // Track which files we've processed (lowercase for case-insensitive matching)
112
132
  const processedFiles = new Set<string>()
113
133
 
114
134
  // Process remote files (remote-only or both sides)
115
135
  for (const remote of remoteFiles) {
116
136
  const normalized = resolveRemoteReference(filesDir, remote.name)
117
- const local = localFileMap.get(normalized.relativePath)
118
- processedFiles.add(normalized.relativePath)
137
+ const normalizedKey = normalizeForComparison(normalized.relativePath)
138
+ const local = localFileMap.get(normalizedKey)
139
+ processedFiles.add(normalizedKey)
119
140
 
120
- const persisted = persistedState?.get(normalized.relativePath)
141
+ const persisted = getPersistedState(normalized.relativePath)
121
142
  const localHash = local ? hashFileContent(local.content) : null
122
143
  const localMatchesPersisted =
123
144
  !!persisted && !!local && localHash === persisted.contentHash
124
145
 
125
146
  if (!local) {
126
- // Remote-only: download
127
- writes.push({
128
- name: normalized.relativePath,
129
- content: remote.content,
130
- modifiedAt: remote.modifiedAt,
131
- })
147
+ // File exists in remote but not locally
148
+ if (persisted) {
149
+ // File was previously synced but now missing locally → deleted locally while offline
150
+ // This is a conflict: local=null (deleted), remote=content
151
+ debug(
152
+ `Conflict: ${normalized.relativePath} deleted locally while offline`
153
+ )
154
+ conflicts.push({
155
+ fileName: normalized.relativePath,
156
+ localContent: null,
157
+ remoteContent: remote.content,
158
+ remoteModifiedAt: remote.modifiedAt,
159
+ lastSyncedAt: persisted?.timestamp,
160
+ })
161
+ } else {
162
+ // New file from remote (never synced before): download
163
+ writes.push({
164
+ name: normalized.relativePath,
165
+ content: remote.content,
166
+ modifiedAt: remote.modifiedAt,
167
+ })
168
+ }
132
169
  continue
133
170
  }
134
171
 
135
172
  if (local.content === remote.content) {
136
- // No change needed
173
+ // Content matches - no disk write needed but track for metadata
174
+ unchanged.push({
175
+ name: normalized.relativePath,
176
+ content: remote.content,
177
+ modifiedAt: remote.modifiedAt,
178
+ })
137
179
  continue
138
180
  }
139
181
 
@@ -163,17 +205,51 @@ export async function detectConflicts(
163
205
 
164
206
  // Process local-only files (not present in remote)
165
207
  for (const local of localFiles) {
166
- if (!processedFiles.has(local.name)) {
167
- // Local-only: upload later
168
- localOnly.push({
169
- name: local.name,
170
- content: local.content,
171
- modifiedAt: local.modifiedAt,
172
- })
208
+ const localKey = normalizeForComparison(local.name)
209
+ if (!processedFiles.has(localKey)) {
210
+ const persisted = getPersistedState(local.name)
211
+ if (persisted) {
212
+ // File was previously synced but now missing from remote → deleted in Framer
213
+ const localHash = hashFileContent(local.content)
214
+ const localClean = localHash === persisted.contentHash
215
+ debug(
216
+ `Conflict: ${local.name} deleted in Framer (localClean=${localClean})`
217
+ )
218
+ conflicts.push({
219
+ fileName: local.name,
220
+ localContent: local.content,
221
+ remoteContent: null,
222
+ localModifiedAt: local.modifiedAt,
223
+ lastSyncedAt: persisted?.timestamp,
224
+ localClean,
225
+ })
226
+ } else {
227
+ // New local file (never synced before): upload later
228
+ localOnly.push({
229
+ name: local.name,
230
+ content: local.content,
231
+ modifiedAt: local.modifiedAt,
232
+ })
233
+ }
234
+ }
235
+ }
236
+
237
+ // Check for files in persisted state that are missing from BOTH sides
238
+ // These were deleted on both sides while offline - auto-clean them (no conflict)
239
+ if (persistedState) {
240
+ for (const [fileName] of persistedState) {
241
+ const normalizedKey = normalizeForComparison(fileName)
242
+ const inLocal = localFileMap.has(normalizedKey)
243
+ const inRemote = remoteFileMap.has(normalizedKey)
244
+ if (!inLocal && !inRemote) {
245
+ debug(`[AUTO-RESOLVE] ${fileName}: deleted on both sides, no conflict`)
246
+ // No action needed - the file is gone from both sides
247
+ // The persisted state will be cleaned up when we persist
248
+ }
173
249
  }
174
250
  }
175
251
 
176
- return { conflicts, writes, localOnly }
252
+ return { conflicts, writes, localOnly, unchanged }
177
253
  }
178
254
 
179
255
  export interface AutoResolveResult {
@@ -199,42 +275,51 @@ export function autoResolveConflicts(
199
275
  for (const conflict of conflicts) {
200
276
  const latestRemoteVersionMs = versionMap.get(conflict.fileName)
201
277
  const lastSyncedAt = conflict.lastSyncedAt
278
+ const localClean = conflict.localClean === true
279
+
280
+ debug(`Auto-resolve checking ${conflict.fileName}`)
202
281
 
203
- info(`[AUTO-RESOLVE] Checking ${conflict.fileName}...`)
282
+ // Remote deletion: file deleted in Framer
283
+ if (conflict.remoteContent === null) {
284
+ if (localClean) {
285
+ debug(` Remote deleted, local clean -> REMOTE (delete locally)`)
286
+ autoResolvedRemote.push(conflict)
287
+ } else {
288
+ debug(` Remote deleted, local modified -> conflict`)
289
+ remainingConflicts.push(conflict)
290
+ }
291
+ continue
292
+ }
204
293
 
205
294
  if (!latestRemoteVersionMs) {
206
- info(`-> No remote version data, keeping conflict`)
295
+ debug(` No remote version data, keeping conflict`)
207
296
  remainingConflicts.push(conflict)
208
297
  continue
209
298
  }
210
299
 
211
300
  if (!lastSyncedAt) {
212
- info(`-> No last sync timestamp, keeping conflict`)
301
+ debug(` No last sync timestamp, keeping conflict`)
213
302
  remainingConflicts.push(conflict)
214
303
  continue
215
304
  }
216
305
 
217
- info(
218
- `-> Latest remote: ${new Date(latestRemoteVersionMs).toISOString()} (${latestRemoteVersionMs})`
219
- )
220
- info(
221
- `-> Last synced: ${new Date(lastSyncedAt).toISOString()} (${lastSyncedAt})`
222
- )
306
+ debug(` Remote: ${new Date(latestRemoteVersionMs).toISOString()}`)
307
+ debug(` Synced: ${new Date(lastSyncedAt).toISOString()}`)
223
308
 
224
309
  const remoteUnchanged =
225
310
  latestRemoteVersionMs <= lastSyncedAt + remoteDriftMs
226
- const localClean = conflict.localClean === true
311
+ // localClean already declared above for remote deletion handling
227
312
 
228
313
  if (remoteUnchanged && !localClean) {
229
- info(` -> Remote unchanged, local changed. Auto-applying LOCAL.`)
314
+ debug(` Remote unchanged, local changed -> LOCAL`)
230
315
  autoResolvedLocal.push(conflict)
231
316
  } else if (localClean && !remoteUnchanged) {
232
- info(` -> Local unchanged, remote changed. Auto-applying REMOTE.`)
317
+ debug(` Local unchanged, remote changed -> REMOTE`)
233
318
  autoResolvedRemote.push(conflict)
234
319
  } else if (remoteUnchanged && localClean) {
235
- info(` -> Both unchanged. Skipping (no conflict).`)
320
+ debug(` Both unchanged, skipping`)
236
321
  } else {
237
- info(` -> Both sides changed. Real conflict.`)
322
+ debug(` Both changed, real conflict`)
238
323
  remainingConflicts.push(conflict)
239
324
  }
240
325
  }
@@ -256,7 +341,7 @@ export async function writeRemoteFiles(
256
341
  hashTracker: HashTracker,
257
342
  installer?: { process: (fileName: string, content: string) => void }
258
343
  ): Promise<void> {
259
- info(`Writing ${files.length} remote files`)
344
+ debug(`Writing ${files.length} remote files`)
260
345
 
261
346
  for (const file of files) {
262
347
  try {
@@ -302,14 +387,14 @@ export async function deleteLocalFile(
302
387
  // Clear the hash immediately
303
388
  hashTracker.forget(normalized.relativePath)
304
389
 
305
- info(`Deleted file: ${normalized.relativePath}`)
390
+ debug(`Deleted file: ${normalized.relativePath}`)
306
391
  } catch (err) {
307
392
  const nodeError = err as NodeJS.ErrnoException
308
393
 
309
394
  if (nodeError?.code === "ENOENT") {
310
395
  // Treat missing files as already deleted to keep hash tracker in sync
311
396
  hashTracker.forget(normalized.relativePath)
312
- info(`File already deleted: ${normalized.relativePath}`)
397
+ debug(`File already deleted: ${normalized.relativePath}`)
313
398
  return
314
399
  }
315
400
 
@@ -1,9 +1,5 @@
1
1
  /**
2
- * Type installer helper
3
- *
4
- * Wraps @typescript/ata with our custom fetcher and initialization routines.
5
- * This preserves the battle-tested logic from the classic CLI while
6
- * conforming to the controller-centric architecture.
2
+ * Type installer helper using @typescript/ata
7
3
  */
8
4
 
9
5
  import { setupTypeAcquisition } from "@typescript/ata"
@@ -11,7 +7,7 @@ import ts from "typescript"
11
7
  import path from "path"
12
8
  import fs from "fs/promises"
13
9
  import { extractImports } from "../utils/imports.js"
14
- import { info, debug, warn } from "../utils/logging.js"
10
+ import { debug, warn } from "../utils/logging.js"
15
11
 
16
12
  export interface InstallerConfig {
17
13
  projectDir: string
@@ -85,7 +81,7 @@ export class Installer {
85
81
 
86
82
  if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
87
83
  seenPackages.add(pkgMatch[1])
88
- info(`📦 Types: ${pkgMatch[1]}`)
84
+ debug(`📦 Types: ${pkgMatch[1]}`)
89
85
  }
90
86
 
91
87
  await this.writeTypeFile(receivedPath, code)
@@ -93,7 +89,7 @@ export class Installer {
93
89
  },
94
90
  })
95
91
 
96
- info("Type installer initialized")
92
+ debug("Type installer initialized")
97
93
  }
98
94
 
99
95
  /**
@@ -181,7 +177,7 @@ export class Installer {
181
177
  }
182
178
 
183
179
  this.processedImports.add(hash)
184
- info(`Processing imports for ${fileName} (${imports.length} packages)`)
180
+ debug(`Processing imports for ${fileName} (${imports.length} packages)`)
185
181
 
186
182
  try {
187
183
  await this.ata(content)
@@ -318,7 +314,7 @@ export class Installer {
318
314
  include: ["files/**/*", "framer-modules.d.ts"],
319
315
  }
320
316
  await fs.writeFile(tsconfigPath, JSON.stringify(config, null, 2))
321
- info("Created tsconfig.json")
317
+ debug("Created tsconfig.json")
322
318
  }
323
319
  }
324
320
 
@@ -334,7 +330,7 @@ export class Installer {
334
330
  trailingComma: "es5",
335
331
  }
336
332
  await fs.writeFile(prettierPath, JSON.stringify(config, null, 2))
337
- info("Created .prettierrc")
333
+ debug("Created .prettierrc")
338
334
  }
339
335
  }
340
336
 
@@ -352,7 +348,7 @@ declare module "https://framerusercontent.com/*"
352
348
  declare module "*.json"
353
349
  `
354
350
  await fs.writeFile(declarationsPath, declarations)
355
- info("Created framer-modules.d.ts")
351
+ debug("Created framer-modules.d.ts")
356
352
  }
357
353
  }
358
354
 
@@ -369,7 +365,7 @@ declare module "*.json"
369
365
  description: "Framer files synced with framer-code-link",
370
366
  }
371
367
  await fs.writeFile(packagePath, JSON.stringify(pkg, null, 2))
372
- info("Created package.json")
368
+ debug("Created package.json")
373
369
  }
374
370
  }
375
371
 
@@ -390,9 +386,9 @@ declare module "*.json"
390
386
  if (
391
387
  await this.hasTypePackage(reactTypesDir, REACT_TYPES_VERSION, reactFiles)
392
388
  ) {
393
- info("📦 React types (from cache)")
389
+ debug("📦 React types (from cache)")
394
390
  } else {
395
- info("Downloading React 18 types for Framer compatibility...")
391
+ debug("Downloading React 18 types...")
396
392
  await this.downloadTypePackage(
397
393
  "@types/react",
398
394
  REACT_TYPES_VERSION,
@@ -415,7 +411,7 @@ declare module "*.json"
415
411
  reactDomFiles
416
412
  )
417
413
  ) {
418
- info("📦 React DOM types (from cache)")
414
+ debug("📦 React DOM types (from cache)")
419
415
  } else {
420
416
  await this.downloadTypePackage(
421
417
  "@types/react-dom",
@@ -8,7 +8,7 @@
8
8
  import type { WebSocket } from "ws"
9
9
  import type { Conflict } from "../types.js"
10
10
  import { sendMessage } from "./connection.js"
11
- import { info, warn } from "../utils/logging.js"
11
+ import { debug, warn } from "../utils/logging.js"
12
12
 
13
13
  class PluginDisconnectedError extends Error {
14
14
  constructor() {
@@ -55,9 +55,7 @@ export class UserActionCoordinator {
55
55
  return await confirmationPromise
56
56
  } catch (err) {
57
57
  if (err instanceof PluginDisconnectedError) {
58
- info(
59
- `[USER-ACTION] Plugin disconnected while waiting for delete confirmation: ${fileName}`
60
- )
58
+ debug(`Plugin disconnected while waiting for delete confirmation: ${fileName}`)
61
59
  return false
62
60
  }
63
61
  throw err
@@ -111,9 +109,7 @@ export class UserActionCoordinator {
111
109
  return new Map(results)
112
110
  } catch (err) {
113
111
  if (err instanceof PluginDisconnectedError) {
114
- info(
115
- "[USER-ACTION] Plugin disconnected while awaiting conflict decisions"
116
- )
112
+ debug("Plugin disconnected while awaiting conflict decisions")
117
113
  return new Map()
118
114
  }
119
115
  throw err
@@ -129,7 +125,7 @@ export class UserActionCoordinator {
129
125
  ): Promise<T> {
130
126
  return new Promise<T>((resolve, reject) => {
131
127
  this.pendingActions.set(actionId, { resolve, reject })
132
- info(`[USER-ACTION] Awaiting ${description}: ${actionId}`)
128
+ debug(`Awaiting ${description}: ${actionId}`)
133
129
  })
134
130
  }
135
131
 
@@ -139,13 +135,13 @@ export class UserActionCoordinator {
139
135
  handleConfirmation<T>(actionId: string, value: T): boolean {
140
136
  const pending = this.pendingActions.get(actionId)
141
137
  if (!pending) {
142
- warn(`[USER-ACTION] Unexpected confirmation for ${actionId}`)
138
+ debug(`Unexpected confirmation for ${actionId}`)
143
139
  return false
144
140
  }
145
141
 
146
142
  this.pendingActions.delete(actionId)
147
143
  pending.resolve(value)
148
- info(`[USER-ACTION] Confirmed: ${actionId}`)
144
+ debug(`Confirmed: ${actionId}`)
149
145
  return true
150
146
  }
151
147
 
@@ -155,7 +151,7 @@ export class UserActionCoordinator {
155
151
  cleanup(): void {
156
152
  for (const [actionId, pending] of this.pendingActions.entries()) {
157
153
  pending.reject(new PluginDisconnectedError())
158
- warn(`[USER-ACTION] Cancelled pending action: ${actionId}`)
154
+ debug(`Cancelled pending action: ${actionId}`)
159
155
  }
160
156
  this.pendingActions.clear()
161
157
  }
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it, vi, afterEach } from "vitest"
2
+ import fs from "fs/promises"
3
+ import os from "os"
4
+ import path from "path"
5
+ import { initWatcher } from "./watcher.js"
6
+
7
+ const createdWatchers: any[] = []
8
+
9
+ vi.mock("chokidar", () => {
10
+ const createMockWatcher = () => {
11
+ const handlers: Record<string, Array<(filePath: string) => any>> = {
12
+ add: [],
13
+ change: [],
14
+ unlink: [],
15
+ }
16
+
17
+ return {
18
+ on(event: "add" | "change" | "unlink", handler: (file: string) => any) {
19
+ handlers[event]?.push(handler)
20
+ return this
21
+ },
22
+ async __emit(event: "add" | "change" | "unlink", filePath: string) {
23
+ for (const handler of handlers[event] ?? []) {
24
+ await handler(filePath)
25
+ }
26
+ },
27
+ close: vi.fn().mockResolvedValue(undefined),
28
+ }
29
+ }
30
+
31
+ const watch = vi.fn(() => {
32
+ const instance = createMockWatcher()
33
+ createdWatchers.push(instance)
34
+ return instance
35
+ })
36
+
37
+ return { default: { watch }, watch }
38
+ })
39
+
40
+ describe("initWatcher", () => {
41
+ afterEach(() => {
42
+ createdWatchers.length = 0
43
+ })
44
+
45
+ it("ignores unsupported extensions and sanitizes added files", async () => {
46
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "framer-watcher-"))
47
+ const events: any[] = []
48
+ const watcher = initWatcher(tmpDir)
49
+ watcher.on("change", (event) => events.push(event))
50
+ const rawWatcher = createdWatchers.at(-1)!
51
+
52
+ const unsupportedPath = path.join(tmpDir, "note.txt")
53
+ await fs.writeFile(unsupportedPath, "hello", "utf-8")
54
+ await rawWatcher.__emit("add", unsupportedPath)
55
+ expect(events).toHaveLength(0)
56
+
57
+ const rawPath = path.join(tmpDir, "bad name!.tsx")
58
+ await fs.writeFile(rawPath, "export const X = 1;", "utf-8")
59
+ await rawWatcher.__emit("add", rawPath)
60
+
61
+ const addEvent = events.find((e) => e.kind === "add")
62
+ expect(addEvent).toBeDefined()
63
+ expect(addEvent?.relativePath).toBe("bad_name_.tsx")
64
+ expect(addEvent?.content).toContain("export const X")
65
+
66
+ const renamedPath = path.join(tmpDir, "bad_name_.tsx")
67
+ await expect(fs.readFile(renamedPath, "utf-8")).resolves.toContain(
68
+ "export const X"
69
+ )
70
+
71
+ await watcher.close()
72
+ await fs.rm(tmpDir, { recursive: true, force: true })
73
+ })
74
+ })
@@ -16,7 +16,7 @@ import {
16
16
  sanitizeFilePath,
17
17
  } from "@code-link/shared"
18
18
  import { getRelativePath } from "../utils/paths.js"
19
- import { info, debug, warn } from "../utils/logging.js"
19
+ import { debug, warn } from "../utils/logging.js"
20
20
 
21
21
  export interface Watcher {
22
22
  on(event: "change", handler: (event: WatcherEvent) => void): void
@@ -35,7 +35,7 @@ export function initWatcher(filesDir: string): Watcher {
35
35
  ignoreInitial: false, // Emit add events for existing files so we can sanitize them
36
36
  })
37
37
 
38
- info(`Watching directory: ${filesDir}`)
38
+ debug(`Watching directory: ${filesDir}`)
39
39
 
40
40
  // Helper to emit normalized events
41
41
  const emitEvent = async (
@@ -62,15 +62,10 @@ export function initWatcher(filesDir: string): Watcher {
62
62
  try {
63
63
  await fs.mkdir(path.dirname(newAbsolutePath), { recursive: true })
64
64
  await fs.rename(absolutePath, newAbsolutePath)
65
- info(
66
- `Renamed ${rawRelativePath} -> ${relativePath} to match Framer rules`
67
- )
65
+ debug(`Renamed ${rawRelativePath} -> ${relativePath}`)
68
66
  effectiveAbsolutePath = newAbsolutePath
69
67
  } catch (err) {
70
- warn(
71
- `Failed to rename ${rawRelativePath} to ${relativePath}, syncing with original name`,
72
- err
73
- )
68
+ warn(`Failed to rename ${rawRelativePath}`, err)
74
69
  }
75
70
  }
76
71
 
package/src/index.ts CHANGED
@@ -10,8 +10,8 @@
10
10
  import { Command } from "commander"
11
11
  import { start } from "./controller.js"
12
12
  import type { Config } from "./types.js"
13
- import { setLogLevel, LogLevel, info } from "./utils/logging.js"
14
- import { getPortFromHash } from "./utils/hashing.js"
13
+ import { setLogLevel, LogLevel, banner, warn } from "./utils/logging.js"
14
+ import { getPortFromHash } from "@code-link/shared"
15
15
  import { getProjectHashFromCwd } from "./utils/project.js"
16
16
 
17
17
  const program = new Command()
@@ -77,6 +77,9 @@ program
77
77
 
78
78
  const port = getPortFromHash(projectHash)
79
79
 
80
+ // Show startup banner
81
+ banner("0.1.3", port)
82
+
80
83
  const config: Config = {
81
84
  port,
82
85
  projectHash,
@@ -88,8 +91,8 @@ program
88
91
  }
89
92
 
90
93
  if (config.dangerouslyAutoDelete) {
91
- info(
92
- "⚠️ Auto-delete mode enabled - files will be deleted without confirmation"
94
+ warn(
95
+ "Auto-delete mode enabled - files will be deleted without confirmation"
93
96
  )
94
97
  }
95
98