framer-code-link 0.2.0 → 0.2.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.
- package/dist/index.mjs +0 -0
- package/package.json +7 -4
- package/src/controller.test.ts +0 -891
- package/src/controller.ts +0 -1419
- package/src/helpers/connection.ts +0 -187
- package/src/helpers/files.test.ts +0 -117
- package/src/helpers/files.ts +0 -463
- package/src/helpers/installer.ts +0 -530
- package/src/helpers/sync-validator.ts +0 -87
- package/src/helpers/user-actions.ts +0 -158
- package/src/helpers/watcher.test.ts +0 -74
- package/src/helpers/watcher.ts +0 -110
- package/src/index.ts +0 -111
- package/src/types.ts +0 -112
- package/src/utils/file-metadata-cache.ts +0 -121
- package/src/utils/hash-tracker.ts +0 -79
- package/src/utils/imports.ts +0 -62
- package/src/utils/logging.ts +0 -235
- package/src/utils/paths.ts +0 -76
- package/src/utils/project.ts +0 -120
- package/src/utils/state-persistence.ts +0 -138
- package/tsconfig.json +0 -14
- package/vitest.config.ts +0 -8
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocket connection helper
|
|
3
|
-
*
|
|
4
|
-
* Thin wrapper around ws.Server that normalizes handshake and surfaces
|
|
5
|
-
* simple callbacks. Keeps raw socket API localized.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { WebSocketServer, WebSocket } from "ws"
|
|
9
|
-
import type { IncomingMessage, OutgoingMessage } from "../types.js"
|
|
10
|
-
import { debug, error } from "../utils/logging.js"
|
|
11
|
-
|
|
12
|
-
export interface ConnectionCallbacks {
|
|
13
|
-
onHandshake: (
|
|
14
|
-
client: WebSocket,
|
|
15
|
-
message: { projectId: string; projectName: string }
|
|
16
|
-
) => void
|
|
17
|
-
onMessage: (message: IncomingMessage) => void
|
|
18
|
-
onDisconnect: () => void
|
|
19
|
-
onError: (error: Error) => void
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface Connection {
|
|
23
|
-
on(event: "handshake", handler: ConnectionCallbacks["onHandshake"]): void
|
|
24
|
-
on(event: "message", handler: ConnectionCallbacks["onMessage"]): void
|
|
25
|
-
on(event: "disconnect", handler: ConnectionCallbacks["onDisconnect"]): void
|
|
26
|
-
on(event: "error", handler: ConnectionCallbacks["onError"]): void
|
|
27
|
-
close(): void
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Initializes a WebSocket server and returns a connection interface
|
|
32
|
-
* Returns a Promise that resolves when the server is ready, or rejects on startup errors
|
|
33
|
-
*/
|
|
34
|
-
export function initConnection(port: number): Promise<Connection> {
|
|
35
|
-
return new Promise((resolve, reject) => {
|
|
36
|
-
const wss = new WebSocketServer({ port })
|
|
37
|
-
const handlers: Partial<ConnectionCallbacks> = {}
|
|
38
|
-
let connectionId = 0
|
|
39
|
-
let isReady = false
|
|
40
|
-
|
|
41
|
-
// Handle server-level errors (e.g., EADDRINUSE)
|
|
42
|
-
wss.on("error", (err: NodeJS.ErrnoException) => {
|
|
43
|
-
if (!isReady) {
|
|
44
|
-
// Startup error - reject the promise with a helpful message
|
|
45
|
-
if (err.code === "EADDRINUSE") {
|
|
46
|
-
error(`Port ${port} is already in use.`)
|
|
47
|
-
error(
|
|
48
|
-
`This usually means another instance of Code Link is already running.`
|
|
49
|
-
)
|
|
50
|
-
error(``)
|
|
51
|
-
error(`To fix this:`)
|
|
52
|
-
error(
|
|
53
|
-
` 1. Close any other terminal running Code Link for this project`
|
|
54
|
-
)
|
|
55
|
-
error(` 2. Or run: lsof -i :${port} | grep LISTEN`)
|
|
56
|
-
error(` Then kill the process: kill -9 <PID>`)
|
|
57
|
-
reject(new Error(`Port ${port} is already in use`))
|
|
58
|
-
} else {
|
|
59
|
-
error(`Failed to start WebSocket server: ${err.message}`)
|
|
60
|
-
reject(err)
|
|
61
|
-
}
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
// Runtime error - log but don't crash
|
|
65
|
-
error(`WebSocket server error: ${err.message}`)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// Server is ready when it starts listening
|
|
69
|
-
wss.on("listening", () => {
|
|
70
|
-
isReady = true
|
|
71
|
-
debug(`WebSocket server listening on port ${port}`)
|
|
72
|
-
|
|
73
|
-
wss.on("connection", (ws: WebSocket) => {
|
|
74
|
-
const connId = ++connectionId
|
|
75
|
-
let handshakeReceived = false
|
|
76
|
-
debug(`Client connected (conn ${connId})`)
|
|
77
|
-
|
|
78
|
-
ws.on("message", (data: Buffer) => {
|
|
79
|
-
try {
|
|
80
|
-
const message = JSON.parse(data.toString()) as IncomingMessage
|
|
81
|
-
|
|
82
|
-
// Special handling for handshake
|
|
83
|
-
if (message.type === "handshake") {
|
|
84
|
-
debug(`Received handshake (conn ${connId})`)
|
|
85
|
-
handshakeReceived = true
|
|
86
|
-
handlers.onHandshake?.(ws, message)
|
|
87
|
-
} else if (handshakeReceived) {
|
|
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
|
-
)
|
|
94
|
-
}
|
|
95
|
-
} catch (err) {
|
|
96
|
-
error(`Failed to parse message:`, err)
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
ws.on("close", (code, reason) => {
|
|
101
|
-
debug(
|
|
102
|
-
`Client disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`
|
|
103
|
-
)
|
|
104
|
-
handlers.onDisconnect?.()
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
ws.on("error", (err) => {
|
|
108
|
-
error(`WebSocket error:`, err)
|
|
109
|
-
})
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
resolve({
|
|
113
|
-
on(
|
|
114
|
-
event: "handshake" | "message" | "disconnect" | "error",
|
|
115
|
-
handler: any
|
|
116
|
-
): void {
|
|
117
|
-
if (event === "handshake") {
|
|
118
|
-
handlers.onHandshake = handler
|
|
119
|
-
} else if (event === "message") {
|
|
120
|
-
handlers.onMessage = handler
|
|
121
|
-
} else if (event === "disconnect") {
|
|
122
|
-
handlers.onDisconnect = handler
|
|
123
|
-
} else if (event === "error") {
|
|
124
|
-
handlers.onError = handler
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
close(): void {
|
|
129
|
-
wss.close()
|
|
130
|
-
},
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* WebSocket readyState constants for reference
|
|
138
|
-
*/
|
|
139
|
-
const READY_STATE = {
|
|
140
|
-
CONNECTING: 0,
|
|
141
|
-
OPEN: 1,
|
|
142
|
-
CLOSING: 2,
|
|
143
|
-
CLOSED: 3,
|
|
144
|
-
} as const
|
|
145
|
-
|
|
146
|
-
function readyStateToString(state: number): string {
|
|
147
|
-
switch (state) {
|
|
148
|
-
case 0:
|
|
149
|
-
return "CONNECTING"
|
|
150
|
-
case 1:
|
|
151
|
-
return "OPEN"
|
|
152
|
-
case 2:
|
|
153
|
-
return "CLOSING"
|
|
154
|
-
case 3:
|
|
155
|
-
return "CLOSED"
|
|
156
|
-
default:
|
|
157
|
-
return `UNKNOWN(${state})`
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Sends a message to a connected socket
|
|
163
|
-
* Returns false if the socket is not open (instead of throwing)
|
|
164
|
-
*/
|
|
165
|
-
export function sendMessage(
|
|
166
|
-
socket: WebSocket,
|
|
167
|
-
message: OutgoingMessage
|
|
168
|
-
): Promise<boolean> {
|
|
169
|
-
return new Promise((resolve) => {
|
|
170
|
-
// Check socket state before attempting to send
|
|
171
|
-
if (socket.readyState !== READY_STATE.OPEN) {
|
|
172
|
-
const stateStr = readyStateToString(socket.readyState)
|
|
173
|
-
debug(`Cannot send ${message.type}: socket is ${stateStr}`)
|
|
174
|
-
resolve(false)
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
socket.send(JSON.stringify(message), (err) => {
|
|
179
|
-
if (err) {
|
|
180
|
-
debug(`Send error for ${message.type}: ${err.message}`)
|
|
181
|
-
resolve(false)
|
|
182
|
-
} else {
|
|
183
|
-
resolve(true)
|
|
184
|
-
}
|
|
185
|
-
})
|
|
186
|
-
})
|
|
187
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import fs from "fs/promises"
|
|
2
|
-
import os from "os"
|
|
3
|
-
import path from "path"
|
|
4
|
-
import { describe, it, expect } from "vitest"
|
|
5
|
-
import { autoResolveConflicts, detectConflicts } from "./files.js"
|
|
6
|
-
import type { Conflict } from "../types.js"
|
|
7
|
-
import { hashFileContent } from "../utils/state-persistence.js"
|
|
8
|
-
|
|
9
|
-
function makeConflict(overrides: Partial<Conflict> = {}): Conflict {
|
|
10
|
-
return {
|
|
11
|
-
fileName: overrides.fileName ?? "Test.tsx",
|
|
12
|
-
localContent: overrides.localContent ?? "local",
|
|
13
|
-
remoteContent: overrides.remoteContent ?? "remote",
|
|
14
|
-
localModifiedAt: overrides.localModifiedAt ?? Date.now(),
|
|
15
|
-
remoteModifiedAt: overrides.remoteModifiedAt ?? Date.now(),
|
|
16
|
-
lastSyncedAt: overrides.lastSyncedAt ?? Date.now(),
|
|
17
|
-
localClean: overrides.localClean,
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("autoResolveConflicts", () => {
|
|
22
|
-
it("classifies conflicts as local when remote unchanged and local changed", () => {
|
|
23
|
-
const conflict = makeConflict({
|
|
24
|
-
lastSyncedAt: 5_000,
|
|
25
|
-
localClean: false,
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const result = autoResolveConflicts(
|
|
29
|
-
[conflict],
|
|
30
|
-
[{ fileName: conflict.fileName, latestRemoteVersionMs: 5_000 }]
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
expect(result.autoResolvedLocal).toHaveLength(1)
|
|
34
|
-
expect(result.autoResolvedRemote).toHaveLength(0)
|
|
35
|
-
expect(result.remainingConflicts).toHaveLength(0)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it("classifies conflicts as remote when local is clean and remote changed", () => {
|
|
39
|
-
const conflict = makeConflict({
|
|
40
|
-
lastSyncedAt: 5_000,
|
|
41
|
-
localClean: true,
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const result = autoResolveConflicts(
|
|
45
|
-
[conflict],
|
|
46
|
-
[{ fileName: conflict.fileName, latestRemoteVersionMs: 10_000 }]
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
expect(result.autoResolvedRemote).toHaveLength(1)
|
|
50
|
-
expect(result.autoResolvedLocal).toHaveLength(0)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it("keeps conflicts that have both sides changed", () => {
|
|
54
|
-
const conflict = makeConflict({
|
|
55
|
-
lastSyncedAt: 5_000,
|
|
56
|
-
localClean: false,
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
const result = autoResolveConflicts(
|
|
60
|
-
[conflict],
|
|
61
|
-
[{ fileName: conflict.fileName, latestRemoteVersionMs: 7_500 }]
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
expect(result.remainingConflicts).toHaveLength(1)
|
|
65
|
-
expect(result.autoResolvedLocal).toHaveLength(0)
|
|
66
|
-
expect(result.autoResolvedRemote).toHaveLength(0)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it("keeps conflicts when version data is missing", () => {
|
|
70
|
-
const conflict = makeConflict({
|
|
71
|
-
lastSyncedAt: 5_000,
|
|
72
|
-
localClean: true,
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
const result = autoResolveConflicts([conflict], [])
|
|
76
|
-
|
|
77
|
-
expect(result.remainingConflicts).toHaveLength(1)
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
describe("detectConflicts", () => {
|
|
82
|
-
it("marks conflicts as localClean when local matches persisted state", async () => {
|
|
83
|
-
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "cl-test-"))
|
|
84
|
-
try {
|
|
85
|
-
const filesDir = path.join(tmpRoot, "files")
|
|
86
|
-
await fs.mkdir(filesDir, { recursive: true })
|
|
87
|
-
|
|
88
|
-
const localContent = "local content"
|
|
89
|
-
await fs.writeFile(path.join(filesDir, "Test.tsx"), localContent, "utf-8")
|
|
90
|
-
|
|
91
|
-
const persistedState = new Map([
|
|
92
|
-
[
|
|
93
|
-
"Test.tsx",
|
|
94
|
-
{ contentHash: hashFileContent(localContent), timestamp: 1_000 },
|
|
95
|
-
],
|
|
96
|
-
])
|
|
97
|
-
|
|
98
|
-
const result = await detectConflicts(
|
|
99
|
-
[
|
|
100
|
-
{
|
|
101
|
-
name: "Test.tsx",
|
|
102
|
-
content: "remote content",
|
|
103
|
-
modifiedAt: 2_000,
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
filesDir,
|
|
107
|
-
{ persistedState }
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
expect(result.writes).toHaveLength(0)
|
|
111
|
-
expect(result.conflicts).toHaveLength(1)
|
|
112
|
-
expect(result.conflicts[0]?.localClean).toBe(true)
|
|
113
|
-
} finally {
|
|
114
|
-
await fs.rm(tmpRoot, { recursive: true, force: true })
|
|
115
|
-
}
|
|
116
|
-
})
|
|
117
|
-
})
|