framer-code-link 0.1.3 → 0.1.4

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.
@@ -1,7 +1,16 @@
1
1
  /**
2
- * Logging utilities for consistent output
2
+ * Logging utilities for consistent, clean CLI output
3
+ *
4
+ * Features:
5
+ * - Log levels (DEBUG, INFO, WARN, ERROR)
6
+ * - Message deduplication with count suffix (x2), (x3)
7
+ * - Reconnect cycle suppression (collapses rapid disconnect/reconnect spam)
8
+ * - Clean prefixes (no [INFO] clutter at default level)
9
+ * - Colored startup banner
3
10
  */
4
11
 
12
+ import pc from "picocolors"
13
+
5
14
  export enum LogLevel {
6
15
  DEBUG = 0,
7
16
  INFO = 1,
@@ -11,37 +20,213 @@ export enum LogLevel {
11
20
 
12
21
  let currentLevel = LogLevel.INFO
13
22
 
23
+ // Deduplication state
24
+ let lastMessage = ""
25
+ let lastMessageCount = 0
26
+ const CLEAR_LINE = "\u001b[2K"
27
+ const MOVE_CURSOR_UP = "\u001b[1A"
28
+
29
+ // Redraw the previous line with the updated message/count.
30
+ function rewriteLastLine(text: string): void {
31
+ if (process.stdout.isTTY) {
32
+ process.stdout.write(`${MOVE_CURSOR_UP}\r${CLEAR_LINE}${text}\n`)
33
+ } else {
34
+ // Fallback for non-TTY (e.g., piping output) – just emit the line.
35
+ process.stdout.write(`${text}\n`)
36
+ }
37
+ }
38
+
39
+ // Reconnect suppression state
40
+ let disconnectTimer: ReturnType<typeof setTimeout> | null = null
41
+ let isShowingDisconnect = false
42
+ let hadRecentDisconnect = false
43
+ // Only show disconnect if down for 4+ seconds
44
+ // Allows for swtiching between Canvas and Code Editor
45
+ const DISCONNECT_DELAY_MS = 4000
46
+
14
47
  export function setLogLevel(level: LogLevel): void {
15
48
  currentLevel = level
16
49
  }
17
50
 
51
+ export function getLogLevel(): LogLevel {
52
+ return currentLevel
53
+ }
54
+
55
+ function dedupeMessage(message: string, count: number): void {
56
+ rewriteLastLine(`${message} ${pc.dim(`(x${count})`)}`)
57
+ }
58
+
59
+ /**
60
+ * Flush any pending deduplicated message
61
+ */
62
+ function flushDedupe(): void {
63
+ if (lastMessageCount > 1) {
64
+ dedupeMessage(lastMessage, lastMessageCount)
65
+ }
66
+ lastMessage = ""
67
+ lastMessageCount = 0
68
+ }
69
+
70
+ /**
71
+ * Log with deduplication - repeated messages within window get counted
72
+ */
73
+ function logWithDedupe(message: string, writer: () => void): void {
74
+ if (message === lastMessage) {
75
+ // Same message - increment count regardless of gap
76
+ lastMessageCount++
77
+ // Overwrite previous line (move cursor up, clear, rewrite)
78
+ dedupeMessage(message, lastMessageCount)
79
+ return
80
+ }
81
+
82
+ lastMessage = message
83
+ lastMessageCount = 1
84
+ writer()
85
+ }
86
+
87
+ /**
88
+ * Print the startup banner - one colored line
89
+ */
90
+ export function banner(version: string, port: number): void {
91
+ console.log()
92
+ let message = ` ${pc.cyan(pc.bold("⚡ Code Link"))} ${pc.dim(`v${version}`)}`
93
+ if (currentLevel <= LogLevel.DEBUG) {
94
+ message += ` ${pc.dim("Port")} ${pc.yellow(port)}`
95
+ }
96
+ console.log(message)
97
+ console.log()
98
+ }
99
+
100
+ /**
101
+ * Debug-level logging - only shown with --verbose flag
102
+ */
18
103
  export function debug(message: string, ...args: unknown[]): void {
19
104
  if (currentLevel <= LogLevel.DEBUG) {
20
- console.debug(`[DEBUG] ${message}`, ...args)
105
+ console.debug(pc.dim(`[DEBUG] ${message}`), ...args)
21
106
  }
22
107
  }
23
108
 
109
+ /**
110
+ * Info-level logging - shown by default, no prefix
111
+ */
24
112
  export function info(message: string, ...args: unknown[]): void {
25
113
  if (currentLevel <= LogLevel.INFO) {
26
- console.info(`[INFO] ${message}`, ...args)
114
+ const formatted = args.length > 0 ? `${message} ${args.join(" ")}` : message
115
+ logWithDedupe(formatted, () => console.log(formatted))
27
116
  }
28
117
  }
29
118
 
119
+ /**
120
+ * Warning-level logging
121
+ */
30
122
  export function warn(message: string, ...args: unknown[]): void {
31
123
  if (currentLevel <= LogLevel.WARN) {
32
- console.warn(`[WARN] ${message}`, ...args)
124
+ flushDedupe()
125
+ console.warn(pc.yellow(`⚠ ${message}`), ...args)
33
126
  }
34
127
  }
35
128
 
129
+ /**
130
+ * Error-level logging
131
+ */
36
132
  export function error(message: string, ...args: unknown[]): void {
37
133
  if (currentLevel <= LogLevel.ERROR) {
38
- console.error(`[ERROR] ${message}`, ...args)
134
+ flushDedupe()
135
+ console.error(pc.red(`✗ ${message}`), ...args)
39
136
  }
40
137
  }
41
138
 
139
+ /**
140
+ * Success message with checkmark
141
+ */
42
142
  export function success(message: string, ...args: unknown[]): void {
43
143
  if (currentLevel <= LogLevel.INFO) {
44
- console.log(`✓ ${message}`, ...args)
144
+ flushDedupe()
145
+ console.log(pc.green(`✓ ${message}`), ...args)
45
146
  }
46
147
  }
47
148
 
149
+ /**
150
+ * File sync indicators
151
+ */
152
+ export function fileDown(fileName: string): void {
153
+ if (currentLevel <= LogLevel.INFO) {
154
+ const msg = ` ${pc.blue("↓")} ${fileName}`
155
+ logWithDedupe(msg, () => console.log(msg))
156
+ }
157
+ }
158
+
159
+ export function fileUp(fileName: string): void {
160
+ if (currentLevel <= LogLevel.INFO) {
161
+ const msg = ` ${pc.green("↑")} ${fileName}`
162
+ logWithDedupe(msg, () => console.log(msg))
163
+ }
164
+ }
165
+
166
+ export function fileDelete(fileName: string): void {
167
+ if (currentLevel <= LogLevel.INFO) {
168
+ const msg = ` ${pc.red("×")} ${fileName}`
169
+ logWithDedupe(msg, () => console.log(msg))
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Status message (dimmed, for "watching for changes..." etc)
175
+ */
176
+ export function status(message: string): void {
177
+ if (currentLevel <= LogLevel.INFO) {
178
+ flushDedupe()
179
+ console.log(pc.dim(` ${message}`))
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Schedule a delayed disconnect message.
185
+ * If reconnection happens before the delay, the message is cancelled.
186
+ */
187
+ export function scheduleDisconnectMessage(callback: () => void): void {
188
+ // Clear any existing timer
189
+ if (disconnectTimer) {
190
+ clearTimeout(disconnectTimer)
191
+ }
192
+
193
+ hadRecentDisconnect = true
194
+ isShowingDisconnect = false
195
+ disconnectTimer = setTimeout(() => {
196
+ isShowingDisconnect = true
197
+ callback()
198
+ disconnectTimer = null
199
+ }, DISCONNECT_DELAY_MS)
200
+ }
201
+
202
+ /**
203
+ * Cancel pending disconnect message (called on reconnect)
204
+ */
205
+ export function cancelDisconnectMessage(): void {
206
+ if (disconnectTimer) {
207
+ clearTimeout(disconnectTimer)
208
+ disconnectTimer = null
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Check if we showed a disconnect message (need to show reconnect)
214
+ */
215
+ export function didShowDisconnect(): boolean {
216
+ return isShowingDisconnect
217
+ }
218
+
219
+ /**
220
+ * Check if we recently saw a disconnect (even if the message was suppressed)
221
+ */
222
+ export function wasRecentlyDisconnected(): boolean {
223
+ return hadRecentDisconnect
224
+ }
225
+
226
+ /**
227
+ * Reset disconnect state after successful reconnect
228
+ */
229
+ export function resetDisconnectState(): void {
230
+ isShowingDisconnect = false
231
+ hadRecentDisconnect = false
232
+ }
@@ -1,8 +1,10 @@
1
1
  import fs from "fs/promises"
2
2
  import path from "path"
3
+ import { shortProjectHash } from "@code-link/shared"
3
4
 
4
5
  interface PackageJson {
5
- framerProjectId?: string
6
+ shortProjectHash?: string // derived short id (8 chars base58)
7
+ framerProjectHash?: string // full 64-char hex hash from Framer API
6
8
  framerProjectName?: string
7
9
  name?: string
8
10
  version?: string
@@ -19,8 +21,8 @@ export function toPackageName(name: string): string {
19
21
 
20
22
  export function toDirName(name: string): string {
21
23
  return name
22
- .replace(/[^a-zA-Z0-9-]/g, "-")
23
- .replace(/^-+|-+$/g, "")
24
+ .replace(/[^a-zA-Z0-9- ]/g, "-")
25
+ .replace(/^[-\s]+|[-\s]+$/g, "")
24
26
  .replace(/-+/g, "-")
25
27
  }
26
28
 
@@ -29,7 +31,8 @@ export async function getProjectHashFromCwd(): Promise<string | null> {
29
31
  const packageJsonPath = path.join(process.cwd(), "package.json")
30
32
  const content = await fs.readFile(packageJsonPath, "utf-8")
31
33
  const pkg = JSON.parse(content) as PackageJson
32
- return pkg.framerProjectId ?? null
34
+ // Return short id for port derivation
35
+ return pkg.shortProjectHash ?? null
33
36
  } catch {
34
37
  return null
35
38
  }
@@ -60,14 +63,16 @@ export async function findOrCreateProjectDir(
60
63
 
61
64
  const dirName = toDirName(projectName)
62
65
  const pkgName = toPackageName(projectName)
63
- const projectDir = path.join(cwd, dirName || projectHash.slice(0, 6))
66
+ const shortId = shortProjectHash(projectHash)
67
+ const projectDir = path.join(cwd, dirName || shortId)
64
68
 
65
69
  await fs.mkdir(path.join(projectDir, "files"), { recursive: true })
66
70
  const pkg: PackageJson = {
67
- name: pkgName || projectHash,
71
+ name: pkgName || shortId,
68
72
  version: "1.0.0",
69
73
  private: true,
70
- framerProjectId: projectHash,
74
+ shortProjectHash: shortId,
75
+ framerProjectHash: projectHash,
71
76
  framerProjectName: projectName,
72
77
  }
73
78
  await fs.writeFile(
@@ -106,7 +111,9 @@ async function matchesProject(
106
111
  try {
107
112
  const content = await fs.readFile(packageJsonPath, "utf-8")
108
113
  const pkg = JSON.parse(content) as PackageJson
109
- return pkg.framerProjectId === projectHash
114
+ const inputShort = shortProjectHash(projectHash)
115
+ // Match on short id (handles both full hash input and short id input)
116
+ return pkg.shortProjectHash === inputShort
110
117
  } catch {
111
118
  return false
112
119
  }