framer-code-link 0.1.1 → 0.1.3

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 CHANGED
@@ -44,50 +44,107 @@ function success(message, ...args) {
44
44
  //#region src/helpers/connection.ts
45
45
  /**
46
46
  * Initializes a WebSocket server and returns a connection interface
47
+ * Returns a Promise that resolves when the server is ready, or rejects on startup errors
47
48
  */
48
49
  function initConnection(port) {
49
- const wss = new WebSocketServer({ port });
50
- const handlers = {};
51
- info(`WebSocket server listening on port ${port}`);
52
- wss.on("connection", (ws) => {
53
- info("Client connected");
54
- ws.on("message", (data) => {
55
- try {
56
- const message = JSON.parse(data.toString());
57
- if (message.type === "handshake") handlers.onHandshake?.(ws, message);
58
- else handlers.onMessage?.(message);
59
- } catch (err) {
60
- error("Failed to parse message:", err);
50
+ return new Promise((resolve, reject) => {
51
+ const wss = new WebSocketServer({ port });
52
+ const handlers = {};
53
+ let connectionId = 0;
54
+ let isReady = false;
55
+ wss.on("error", (err) => {
56
+ if (!isReady) {
57
+ if (err.code === "EADDRINUSE") {
58
+ error(`Port ${port} is already in use.`);
59
+ error(`This usually means another instance of Code Link is already running.`);
60
+ error(``);
61
+ error(`To fix this:`);
62
+ error(` 1. Close any other terminal running Code Link for this project`);
63
+ error(` 2. Or run: lsof -i :${port} | grep LISTEN`);
64
+ error(` Then kill the process: kill -9 <PID>`);
65
+ reject(new Error(`Port ${port} is already in use`));
66
+ } else {
67
+ error(`Failed to start WebSocket server: ${err.message}`);
68
+ reject(err);
69
+ }
70
+ return;
61
71
  }
72
+ error(`WebSocket server error: ${err.message}`);
62
73
  });
63
- ws.on("close", () => {
64
- info("Client disconnected");
65
- handlers.onDisconnect?.();
66
- });
67
- ws.on("error", (err) => {
68
- error("WebSocket error:", err);
74
+ wss.on("listening", () => {
75
+ isReady = true;
76
+ info(`WebSocket server listening on port ${port}`);
77
+ wss.on("connection", (ws) => {
78
+ const connId = ++connectionId;
79
+ info(`[CONN ${connId}] Client connected (readyState: ${ws.readyState})`);
80
+ ws.on("message", (data) => {
81
+ try {
82
+ const message = JSON.parse(data.toString());
83
+ if (message.type === "handshake") {
84
+ info(`[CONN ${connId}] Received handshake`);
85
+ handlers.onHandshake?.(ws, message);
86
+ } else handlers.onMessage?.(message);
87
+ } catch (err) {
88
+ error(`[CONN ${connId}] Failed to parse message:`, err);
89
+ }
90
+ });
91
+ ws.on("close", (code, reason) => {
92
+ info(`[CONN ${connId}] Client disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`);
93
+ handlers.onDisconnect?.();
94
+ });
95
+ ws.on("error", (err) => {
96
+ error(`[CONN ${connId}] WebSocket error:`, err);
97
+ });
98
+ });
99
+ resolve({
100
+ on(event, handler) {
101
+ if (event === "handshake") handlers.onHandshake = handler;
102
+ else if (event === "message") handlers.onMessage = handler;
103
+ else if (event === "disconnect") handlers.onDisconnect = handler;
104
+ else if (event === "error") handlers.onError = handler;
105
+ },
106
+ close() {
107
+ wss.close();
108
+ }
109
+ });
69
110
  });
70
111
  });
71
- return {
72
- on(event, handler) {
73
- if (event === "handshake") handlers.onHandshake = handler;
74
- else if (event === "message") handlers.onMessage = handler;
75
- else if (event === "disconnect") handlers.onDisconnect = handler;
76
- else if (event === "error") handlers.onError = handler;
77
- },
78
- close() {
79
- wss.close();
80
- }
81
- };
112
+ }
113
+ /**
114
+ * WebSocket readyState constants for reference
115
+ */
116
+ const READY_STATE = {
117
+ CONNECTING: 0,
118
+ OPEN: 1,
119
+ CLOSING: 2,
120
+ CLOSED: 3
121
+ };
122
+ function readyStateToString(state) {
123
+ switch (state) {
124
+ case 0: return "CONNECTING";
125
+ case 1: return "OPEN";
126
+ case 2: return "CLOSING";
127
+ case 3: return "CLOSED";
128
+ default: return `UNKNOWN(${state})`;
129
+ }
82
130
  }
83
131
  /**
84
132
  * Sends a message to a connected socket
133
+ * Returns false if the socket is not open (instead of throwing)
85
134
  */
86
135
  function sendMessage(socket, message) {
87
- return new Promise((resolve, reject) => {
136
+ return new Promise((resolve) => {
137
+ if (socket.readyState !== READY_STATE.OPEN) {
138
+ const stateStr = readyStateToString(socket.readyState);
139
+ info(`[WS] Cannot send ${message.type}: socket is ${stateStr}`);
140
+ resolve(false);
141
+ return;
142
+ }
88
143
  socket.send(JSON.stringify(message), (err) => {
89
- if (err) reject(err);
90
- else resolve();
144
+ if (err) {
145
+ error(`[WS] Send error for ${message.type}:`, err.message);
146
+ resolve(false);
147
+ } else resolve(true);
91
148
  });
92
149
  });
93
150
  }
@@ -1243,6 +1300,19 @@ function validateIncomingChange(file, fileMeta, currentMode) {
1243
1300
  function toPackageName(name) {
1244
1301
  return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
1245
1302
  }
1303
+ function toDirName(name) {
1304
+ return name.replace(/[^a-zA-Z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
1305
+ }
1306
+ async function getProjectHashFromCwd() {
1307
+ try {
1308
+ const packageJsonPath = path.join(process.cwd(), "package.json");
1309
+ const content = await fs.readFile(packageJsonPath, "utf-8");
1310
+ const pkg = JSON.parse(content);
1311
+ return pkg.framerProjectId ?? null;
1312
+ } catch {
1313
+ return null;
1314
+ }
1315
+ }
1246
1316
  async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
1247
1317
  if (explicitDir) {
1248
1318
  const resolved = path.resolve(explicitDir);
@@ -1253,11 +1323,12 @@ async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
1253
1323
  const existing = await findExistingProjectDir(cwd, projectHash);
1254
1324
  if (existing) return existing;
1255
1325
  if (!projectName) throw new Error("Project name is required when creating a new workspace. Pass --name <project name>.");
1256
- const dirName = toPackageName(projectName);
1326
+ const dirName = toDirName(projectName);
1327
+ const pkgName = toPackageName(projectName);
1257
1328
  const projectDir = path.join(cwd, dirName || projectHash.slice(0, 6));
1258
1329
  await fs.mkdir(path.join(projectDir, "files"), { recursive: true });
1259
1330
  const pkg = {
1260
- name: dirName || projectHash,
1331
+ name: pkgName || projectHash,
1261
1332
  version: "1.0.0",
1262
1333
  private: true,
1263
1334
  framerProjectId: projectHash,
@@ -1697,7 +1768,10 @@ async function executeEffect(effect, context) {
1697
1768
  }];
1698
1769
  }
1699
1770
  case "SEND_MESSAGE": {
1700
- if (syncState.socket) await sendMessage(syncState.socket, effect.payload);
1771
+ if (syncState.socket) {
1772
+ const sent = await sendMessage(syncState.socket, effect.payload);
1773
+ if (!sent) warn(`Failed to send message: ${effect.payload.type}`);
1774
+ } else warn(`No socket available to send: ${effect.payload.type}`);
1701
1775
  return [];
1702
1776
  }
1703
1777
  case "WRITE_FILES": {
@@ -1827,9 +1901,14 @@ async function start(config) {
1827
1901
  };
1828
1902
  const userActions = new UserActionCoordinator();
1829
1903
  async function processEvent(event) {
1904
+ const socketState = syncState.socket?.readyState;
1905
+ info(`[STATE] Processing event: ${event.type} (mode: ${syncState.mode}, socket: ${socketState ?? "none"})`);
1830
1906
  const result = transition(syncState, event);
1831
1907
  syncState = result.state;
1908
+ if (result.effects.length > 0) info(`[STATE] Event produced ${result.effects.length} effects: ${result.effects.map((e) => e.type).join(", ")}`);
1832
1909
  for (const effect of result.effects) {
1910
+ const currentSocketState = syncState.socket?.readyState;
1911
+ if (currentSocketState !== void 0 && currentSocketState !== 1) warn(`[STATE] Socket not open (state: ${currentSocketState}) before executing ${effect.type}`);
1833
1912
  const followUpEvents = await executeEffect(effect, {
1834
1913
  config,
1835
1914
  hashTracker,
@@ -1841,7 +1920,7 @@ async function start(config) {
1841
1920
  for (const followUpEvent of followUpEvents) await processEvent(followUpEvent);
1842
1921
  }
1843
1922
  }
1844
- const connection = initConnection(config.port);
1923
+ const connection = await initConnection(config.port);
1845
1924
  connection.on("handshake", async (client, message) => {
1846
1925
  info("Received handshake from plugin");
1847
1926
  info(`Project: ${message.projectName} (${message.projectId})`);
@@ -1875,12 +1954,15 @@ async function start(config) {
1875
1954
  case "request-files":
1876
1955
  event = { type: "REQUEST_FILES" };
1877
1956
  break;
1878
- case "file-list":
1957
+ case "file-list": {
1958
+ const totalSize = message.files.reduce((sum, f) => sum + (f.content?.length ?? 0), 0);
1959
+ info(`[FILE_LIST] Received ${message.files.length} files (${(totalSize / 1024).toFixed(1)}KB total)`);
1879
1960
  event = {
1880
1961
  type: "FILE_LIST",
1881
1962
  files: message.files
1882
1963
  };
1883
1964
  break;
1965
+ }
1884
1966
  case "file-change":
1885
1967
  event = {
1886
1968
  type: "FILE_CHANGE",
@@ -1994,7 +2076,16 @@ program.exitOverride((err) => {
1994
2076
  }
1995
2077
  throw err;
1996
2078
  });
1997
- program.name("code-link").description("Sync Framer code components to your local filesystem").version("0.1.0").argument("<projectHash>", "Framer Project ID Hash").option("-n, --name <name>", "Project name (optional)").option("-d, --dir <directory>", "Explicit project directory").option("-v, --verbose", "Enable verbose logging").option("--log-level <level>", "Set log level (debug, info, warn, error)").option("--dangerously-auto-delete", "Automatically delete remote files without confirmation").action(async (projectHash, options) => {
2079
+ program.name("code-link").description("Sync Framer code components to your local filesystem").version("0.1.0").argument("[projectHash]", "Framer Project ID Hash (auto-detected from package.json if omitted)").option("-n, --name <name>", "Project name (optional)").option("-d, --dir <directory>", "Explicit project directory").option("-v, --verbose", "Enable verbose logging").option("--log-level <level>", "Set log level (debug, info, warn, error)").option("--dangerously-auto-delete", "Automatically delete remote files without confirmation").action(async (projectHash, options) => {
2080
+ if (!projectHash) {
2081
+ const detected = await getProjectHashFromCwd();
2082
+ if (detected) projectHash = detected;
2083
+ else {
2084
+ console.error("No project ID provided and no framerProjectId found in package.json.");
2085
+ console.error("Either run this command from a project directory or copy the command from the Code Link Plugin.");
2086
+ process.exit(1);
2087
+ }
2088
+ }
1998
2089
  const isDev = process.env.NODE_ENV === "development";
1999
2090
  if (options.logLevel) {
2000
2091
  const levelMap = {
@@ -2017,7 +2108,11 @@ program.name("code-link").description("Sync Framer code components to your local
2017
2108
  explicitName: options.name
2018
2109
  };
2019
2110
  if (config.dangerouslyAutoDelete) info("⚠️ Auto-delete mode enabled - files will be deleted without confirmation");
2020
- await start(config);
2111
+ try {
2112
+ await start(config);
2113
+ } catch (err) {
2114
+ process.exit(1);
2115
+ }
2021
2116
  });
2022
2117
  program.parse();
2023
2118
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-code-link",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "CLI tool for syncing Framer code components - controller-centric architecture",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
package/src/controller.ts CHANGED
@@ -760,7 +760,12 @@ async function executeEffect(
760
760
 
761
761
  case "SEND_MESSAGE": {
762
762
  if (syncState.socket) {
763
- await sendMessage(syncState.socket, effect.payload)
763
+ const sent = await sendMessage(syncState.socket, effect.payload)
764
+ if (!sent) {
765
+ warn(`Failed to send message: ${effect.payload.type}`)
766
+ }
767
+ } else {
768
+ warn(`No socket available to send: ${effect.payload.type}`)
764
769
  }
765
770
  return []
766
771
  }
@@ -976,11 +981,30 @@ export async function start(config: Config): Promise<void> {
976
981
  // State Machine Execution Helper
977
982
  // Process events through state machine and execute effects recursively
978
983
  async function processEvent(event: SyncEvent) {
984
+ const socketState = syncState.socket?.readyState
985
+ info(
986
+ `[STATE] Processing event: ${event.type} (mode: ${syncState.mode}, socket: ${socketState ?? "none"})`
987
+ )
988
+
979
989
  const result = transition(syncState, event)
980
990
  syncState = result.state
981
991
 
992
+ if (result.effects.length > 0) {
993
+ info(
994
+ `[STATE] Event produced ${result.effects.length} effects: ${result.effects.map((e) => e.type).join(", ")}`
995
+ )
996
+ }
997
+
982
998
  // Execute all effects and process any follow-up events
983
999
  for (const effect of result.effects) {
1000
+ // Check socket state before each effect
1001
+ const currentSocketState = syncState.socket?.readyState
1002
+ if (currentSocketState !== undefined && currentSocketState !== 1) {
1003
+ warn(
1004
+ `[STATE] Socket not open (state: ${currentSocketState}) before executing ${effect.type}`
1005
+ )
1006
+ }
1007
+
984
1008
  const followUpEvents = await executeEffect(effect, {
985
1009
  config,
986
1010
  hashTracker,
@@ -998,7 +1022,7 @@ export async function start(config: Config): Promise<void> {
998
1022
  }
999
1023
 
1000
1024
  // WebSocket Connection
1001
- const connection = initConnection(config.port)
1025
+ const connection = await initConnection(config.port)
1002
1026
 
1003
1027
  // Handle initial handshake
1004
1028
  connection.on("handshake", async (client: WebSocket, message) => {
@@ -1051,9 +1075,17 @@ export async function start(config: Config): Promise<void> {
1051
1075
  event = { type: "REQUEST_FILES" }
1052
1076
  break
1053
1077
 
1054
- case "file-list":
1078
+ case "file-list": {
1079
+ const totalSize = message.files.reduce(
1080
+ (sum, f) => sum + (f.content?.length ?? 0),
1081
+ 0
1082
+ )
1083
+ info(
1084
+ `[FILE_LIST] Received ${message.files.length} files (${(totalSize / 1024).toFixed(1)}KB total)`
1085
+ )
1055
1086
  event = { type: "FILE_LIST", files: message.files }
1056
1087
  break
1088
+ }
1057
1089
 
1058
1090
  case "file-change":
1059
1091
  event = {
@@ -29,74 +29,152 @@ export interface Connection {
29
29
 
30
30
  /**
31
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
32
33
  */
33
- export function initConnection(port: number): Connection {
34
- const wss = new WebSocketServer({ port })
35
- const handlers: Partial<ConnectionCallbacks> = {}
36
-
37
- info(`WebSocket server listening on port ${port}`)
38
-
39
- wss.on("connection", (ws: WebSocket) => {
40
- info("Client connected")
41
-
42
- ws.on("message", (data: Buffer) => {
43
- try {
44
- const message = JSON.parse(data.toString()) as IncomingMessage
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
45
40
 
46
- // Special handling for handshake
47
- if (message.type === "handshake") {
48
- handlers.onHandshake?.(ws, message)
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`))
49
58
  } else {
50
- handlers.onMessage?.(message)
59
+ error(`Failed to start WebSocket server: ${err.message}`)
60
+ reject(err)
51
61
  }
52
- } catch (err) {
53
- error("Failed to parse message:", err)
62
+ return
54
63
  }
64
+ // Runtime error - log but don't crash
65
+ error(`WebSocket server error: ${err.message}`)
55
66
  })
56
67
 
57
- ws.on("close", () => {
58
- info("Client disconnected")
59
- handlers.onDisconnect?.()
60
- })
68
+ // Server is ready when it starts listening
69
+ wss.on("listening", () => {
70
+ isReady = true
71
+ info(`WebSocket server listening on port ${port}`)
72
+
73
+ wss.on("connection", (ws: WebSocket) => {
74
+ const connId = ++connectionId
75
+ info(`[CONN ${connId}] Client connected (readyState: ${ws.readyState})`)
76
+
77
+ ws.on("message", (data: Buffer) => {
78
+ try {
79
+ const message = JSON.parse(data.toString()) as IncomingMessage
80
+
81
+ // Special handling for handshake
82
+ if (message.type === "handshake") {
83
+ info(`[CONN ${connId}] Received handshake`)
84
+ handlers.onHandshake?.(ws, message)
85
+ } else {
86
+ handlers.onMessage?.(message)
87
+ }
88
+ } catch (err) {
89
+ error(`[CONN ${connId}] Failed to parse message:`, err)
90
+ }
91
+ })
92
+
93
+ ws.on("close", (code, reason) => {
94
+ info(
95
+ `[CONN ${connId}] Client disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`
96
+ )
97
+ handlers.onDisconnect?.()
98
+ })
61
99
 
62
- ws.on("error", (err) => {
63
- error("WebSocket error:", err)
100
+ ws.on("error", (err) => {
101
+ error(`[CONN ${connId}] WebSocket error:`, err)
102
+ })
103
+ })
104
+
105
+ resolve({
106
+ on(
107
+ event: "handshake" | "message" | "disconnect" | "error",
108
+ handler: any
109
+ ): void {
110
+ if (event === "handshake") {
111
+ handlers.onHandshake = handler
112
+ } else if (event === "message") {
113
+ handlers.onMessage = handler
114
+ } else if (event === "disconnect") {
115
+ handlers.onDisconnect = handler
116
+ } else if (event === "error") {
117
+ handlers.onError = handler
118
+ }
119
+ },
120
+
121
+ close(): void {
122
+ wss.close()
123
+ },
124
+ })
64
125
  })
65
126
  })
127
+ }
66
128
 
67
- return {
68
- on(
69
- event: "handshake" | "message" | "disconnect" | "error",
70
- handler: any
71
- ): void {
72
- if (event === "handshake") {
73
- handlers.onHandshake = handler
74
- } else if (event === "message") {
75
- handlers.onMessage = handler
76
- } else if (event === "disconnect") {
77
- handlers.onDisconnect = handler
78
- } else if (event === "error") {
79
- handlers.onError = handler
80
- }
81
- },
129
+ /**
130
+ * WebSocket readyState constants for reference
131
+ */
132
+ const READY_STATE = {
133
+ CONNECTING: 0,
134
+ OPEN: 1,
135
+ CLOSING: 2,
136
+ CLOSED: 3,
137
+ } as const
82
138
 
83
- close(): void {
84
- wss.close()
85
- },
139
+ function readyStateToString(state: number): string {
140
+ switch (state) {
141
+ case 0:
142
+ return "CONNECTING"
143
+ case 1:
144
+ return "OPEN"
145
+ case 2:
146
+ return "CLOSING"
147
+ case 3:
148
+ return "CLOSED"
149
+ default:
150
+ return `UNKNOWN(${state})`
86
151
  }
87
152
  }
88
153
 
89
154
  /**
90
155
  * Sends a message to a connected socket
156
+ * Returns false if the socket is not open (instead of throwing)
91
157
  */
92
158
  export function sendMessage(
93
159
  socket: WebSocket,
94
160
  message: OutgoingMessage
95
- ): Promise<void> {
96
- return new Promise((resolve, reject) => {
161
+ ): Promise<boolean> {
162
+ return new Promise((resolve) => {
163
+ // Check socket state before attempting to send
164
+ if (socket.readyState !== READY_STATE.OPEN) {
165
+ const stateStr = readyStateToString(socket.readyState)
166
+ info(`[WS] Cannot send ${message.type}: socket is ${stateStr}`)
167
+ resolve(false)
168
+ return
169
+ }
170
+
97
171
  socket.send(JSON.stringify(message), (err) => {
98
- if (err) reject(err)
99
- else resolve()
172
+ if (err) {
173
+ error(`[WS] Send error for ${message.type}:`, err.message)
174
+ resolve(false)
175
+ } else {
176
+ resolve(true)
177
+ }
100
178
  })
101
179
  })
102
180
  }
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import { start } from "./controller.js"
12
12
  import type { Config } from "./types.js"
13
13
  import { setLogLevel, LogLevel, info } from "./utils/logging.js"
14
14
  import { getPortFromHash } from "./utils/hashing.js"
15
+ import { getProjectHashFromCwd } from "./utils/project.js"
15
16
 
16
17
  const program = new Command()
17
18
 
@@ -27,7 +28,10 @@ program
27
28
  .name("code-link")
28
29
  .description("Sync Framer code components to your local filesystem")
29
30
  .version("0.1.0")
30
- .argument("<projectHash>", "Framer Project ID Hash")
31
+ .argument(
32
+ "[projectHash]",
33
+ "Framer Project ID Hash (auto-detected from package.json if omitted)"
34
+ )
31
35
  .option("-n, --name <name>", "Project name (optional)")
32
36
  .option("-d, --dir <directory>", "Explicit project directory")
33
37
  .option("-v, --verbose", "Enable verbose logging")
@@ -36,7 +40,23 @@ program
36
40
  "--dangerously-auto-delete",
37
41
  "Automatically delete remote files without confirmation"
38
42
  )
39
- .action(async (projectHash: string, options) => {
43
+ .action(async (projectHash: string | undefined, options) => {
44
+ // If no projectHash provided, try to read from cwd's package.json
45
+ if (!projectHash) {
46
+ const detected = await getProjectHashFromCwd()
47
+ if (detected) {
48
+ projectHash = detected
49
+ } else {
50
+ console.error(
51
+ "No project ID provided and no framerProjectId found in package.json."
52
+ )
53
+ console.error(
54
+ "Either run this command from a project directory or copy the command from the Code Link Plugin."
55
+ )
56
+ process.exit(1)
57
+ }
58
+ }
59
+
40
60
  // Auto-enable debug in development unless overridden
41
61
  const isDev = process.env.NODE_ENV === "development"
42
62
 
@@ -73,7 +93,12 @@ program
73
93
  )
74
94
  }
75
95
 
76
- await start(config)
96
+ try {
97
+ await start(config)
98
+ } catch (err) {
99
+ // Error already logged, exit cleanly
100
+ process.exit(1)
101
+ }
77
102
  })
78
103
 
79
104
  program.parse()
@@ -17,6 +17,24 @@ export function toPackageName(name: string): string {
17
17
  .replace(/-+/g, "-")
18
18
  }
19
19
 
20
+ export function toDirName(name: string): string {
21
+ return name
22
+ .replace(/[^a-zA-Z0-9-]/g, "-")
23
+ .replace(/^-+|-+$/g, "")
24
+ .replace(/-+/g, "-")
25
+ }
26
+
27
+ export async function getProjectHashFromCwd(): Promise<string | null> {
28
+ try {
29
+ const packageJsonPath = path.join(process.cwd(), "package.json")
30
+ const content = await fs.readFile(packageJsonPath, "utf-8")
31
+ const pkg = JSON.parse(content) as PackageJson
32
+ return pkg.framerProjectId ?? null
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
20
38
  export async function findOrCreateProjectDir(
21
39
  projectHash: string,
22
40
  projectName?: string,
@@ -40,12 +58,13 @@ export async function findOrCreateProjectDir(
40
58
  )
41
59
  }
42
60
 
43
- const dirName = toPackageName(projectName)
61
+ const dirName = toDirName(projectName)
62
+ const pkgName = toPackageName(projectName)
44
63
  const projectDir = path.join(cwd, dirName || projectHash.slice(0, 6))
45
64
 
46
65
  await fs.mkdir(path.join(projectDir, "files"), { recursive: true })
47
66
  const pkg: PackageJson = {
48
- name: dirName || projectHash,
67
+ name: pkgName || projectHash,
49
68
  version: "1.0.0",
50
69
  private: true,
51
70
  framerProjectId: projectHash,