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.
- package/dist/index.js +551 -144
- package/package.json +12 -12
- package/src/controller.test.ts +22 -137
- package/src/controller.ts +242 -106
- package/src/helpers/connection.ts +10 -10
- package/src/helpers/files.ts +99 -37
- package/src/helpers/installer.ts +11 -11
- package/src/helpers/user-actions.ts +7 -11
- package/src/helpers/watcher.ts +4 -9
- package/src/index.ts +7 -4
- package/src/types.ts +4 -2
- package/src/utils/{hashing.ts → hash-tracker.ts} +1 -17
- package/src/utils/logging.ts +191 -6
- package/src/utils/project.ts +15 -8
package/src/utils/logging.ts
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/utils/project.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
71
|
+
name: pkgName || shortId,
|
|
68
72
|
version: "1.0.0",
|
|
69
73
|
private: true,
|
|
70
|
-
|
|
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
|
-
|
|
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
|
}
|