From b38dc34a3c7a9e90bb92776bc4004dae66c593ba Mon Sep 17 00:00:00 2001 From: Hunter Caron Date: Thu, 12 Feb 2026 11:30:58 +0100 Subject: [PATCH 1/7] Update readme --- plugins/code-link/README.md | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/plugins/code-link/README.md b/plugins/code-link/README.md index d842fb2ce..30a27cbad 100644 --- a/plugins/code-link/README.md +++ b/plugins/code-link/README.md @@ -1,13 +1,30 @@ # Framer Code Link -Plugin that syncs code files between Framer and your local filesystem via the Code Link CLI. +Two-way sync between Framer code components and your local filesystem. -**By:** @huntercaron +**By:** [@huntercaron](https://github.com/huntercaron) -![Code Link Image](../../assets/code-link.png) +Code Link -## Usage +## Features -1. Open the Plugin in Framer +- **Real-time two-way sync** — Edits in locally instantly appear in Framer, and vice-versa +- **Automatic types** — TypeScript types for `framer`, `framer-motion`, `react` are automatically installed +- **Smart conflict resolution** — Auto-resolves when safe, prompts you to choose when both sides change +- **Project scaffolding** — Creates project files on first run; re-run with just `npx framer-code-link` +- **AI skill** — Installs Framer component best-practices for Cursor, Claude, and other AI editors + +## Quick Start + +1. Open the **Code Link** Plugin in your Framer project 2. Copy the CLI command shown in the Plugin -3. Run the command in your terminal to start syncing +3. Paste and run the command in your terminal +4. Edit files in `{project}/files/` — changes sync to Framer + +## CLI Options + +| Flag | Description | +| --- | --- | +| `-n, --name ` | Project name for the created directory | +| `-d, --dir ` | Target project directory | +| `-v, --verbose` | Enable debug logging | \ No newline at end of file From 5b4a0c1f57980e0e239961cc211d8531b131045d Mon Sep 17 00:00:00 2001 From: Hunter Caron Date: Thu, 12 Feb 2026 11:31:17 +0100 Subject: [PATCH 2/7] Add logging for folder info --- packages/code-link-cli/package.json | 2 +- packages/code-link-cli/src/controller.ts | 25 +++++++++++++++++---- packages/code-link-cli/src/types.ts | 1 + packages/code-link-cli/src/utils/project.ts | 8 +++---- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/code-link-cli/package.json b/packages/code-link-cli/package.json index ce92fb316..f7e476f5e 100644 --- a/packages/code-link-cli/package.json +++ b/packages/code-link-cli/package.json @@ -1,6 +1,6 @@ { "name": "framer-code-link", - "version": "0.7.0", + "version": "0.8.0", "description": "CLI tool for syncing Framer code components - controller-centric architecture", "main": "dist/index.mjs", "type": "module", diff --git a/packages/code-link-cli/src/controller.ts b/packages/code-link-cli/src/controller.ts index 03eac1002..b23ca4624 100644 --- a/packages/code-link-cli/src/controller.ts +++ b/packages/code-link-cli/src/controller.ts @@ -8,6 +8,7 @@ import type { CliToPluginMessage, PluginToCliMessage } from "@code-link/shared" import { pluralize, shortProjectHash } from "@code-link/shared" import fs from "fs/promises" +import path from "path" import type { WebSocket } from "ws" import { initConnection, sendMessage } from "./helpers/connection.ts" import { @@ -681,7 +682,9 @@ async function executeEffect( if (!config.projectDir) { const projectName = config.explicitName ?? effect.projectInfo.projectName - config.projectDir = await findOrCreateProjectDir(config.projectHash, projectName, config.explicitDir) + const result = await findOrCreateProjectDir(config.projectHash, projectName, config.explicitDir) + config.projectDir = result.dir + config.projectDirCreated = result.created // May allow customization of file directory in the future config.filesDir = `${config.projectDir}/files` @@ -950,9 +953,23 @@ async function executeEffect( return [] } - success( - `Synced ${effect.totalCount} files (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)` - ) + const relativeDir = config.projectDir ? "./" + (path.relative(process.cwd(), config.projectDir) || ".") : null + + if (effect.totalCount === 0 && relativeDir) { + if (config.projectDirCreated) { + success(`Created ${relativeDir} folder`) + } else { + success(`Syncing to ${relativeDir} folder`) + } + } else if (relativeDir) { + success( + `Synced into ${relativeDir} (${effect.updatedCount} files updated, ${effect.unchangedCount} unchanged)` + ) + } else { + success( + `Synced ${effect.totalCount} files (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)` + ) + } status("Watching for changes...") return [] } diff --git a/packages/code-link-cli/src/types.ts b/packages/code-link-cli/src/types.ts index 4283906ad..1a080ff99 100644 --- a/packages/code-link-cli/src/types.ts +++ b/packages/code-link-cli/src/types.ts @@ -21,6 +21,7 @@ export interface Config { allowUnsupportedNpm: boolean // Allow type acquisition for unsupported npm packages explicitDir?: string // User-provided directory override explicitName?: string // User-provided name override + projectDirCreated?: boolean // Whether the project directory was newly created } // Local file representation (CLI-specific) diff --git a/packages/code-link-cli/src/utils/project.ts b/packages/code-link-cli/src/utils/project.ts index f343c08a3..044be2568 100644 --- a/packages/code-link-cli/src/utils/project.ts +++ b/packages/code-link-cli/src/utils/project.ts @@ -40,17 +40,17 @@ export async function findOrCreateProjectDir( projectHash: string, projectName?: string, explicitDir?: string -): Promise { +): Promise<{ dir: string; created: boolean }> { if (explicitDir) { const resolved = path.resolve(explicitDir) await fs.mkdir(path.join(resolved, "files"), { recursive: true }) - return resolved + return { dir: resolved, created: false } } const cwd = process.cwd() const existing = await findExistingProjectDir(cwd, projectHash) if (existing) { - return existing + return { dir: existing, created: false } } if (!projectName) { @@ -72,7 +72,7 @@ export async function findOrCreateProjectDir( } await fs.writeFile(path.join(projectDir, "package.json"), JSON.stringify(pkg, null, 2)) - return projectDir + return { dir: projectDir, created: true } } async function findExistingProjectDir(baseDir: string, projectHash: string): Promise { From 95e0721c64d5a4a172ea8c17c8d01a64069bcd10 Mon Sep 17 00:00:00 2001 From: Hunter Caron Date: Thu, 12 Feb 2026 11:55:16 +0100 Subject: [PATCH 3/7] Polish --- packages/code-link-cli/src/controller.ts | 24 ++++++++++++++---------- plugins/code-link/README.md | 8 ++++---- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/code-link-cli/src/controller.ts b/packages/code-link-cli/src/controller.ts index b23ca4624..4551f5586 100644 --- a/packages/code-link-cli/src/controller.ts +++ b/packages/code-link-cli/src/controller.ts @@ -961,6 +961,10 @@ async function executeEffect( } else { success(`Syncing to ${relativeDir} folder`) } + } else if (relativeDir && config.projectDirCreated) { + success( + `Synced into ${relativeDir} (${effect.updatedCount} files added)` + ) } else if (relativeDir) { success( `Synced into ${relativeDir} (${effect.updatedCount} files updated, ${effect.unchangedCount} unchanged)` @@ -1058,6 +1062,16 @@ export async function start(config: Config): Promise { } void (async () => { + // Cancel any pending disconnect message (fast reconnect) + cancelDisconnectMessage() + + // Only show "Connected" on initial connection, not reconnects + // Reconnect confirmation happens in SYNC_COMPLETE + const wasDisconnected = wasRecentlyDisconnected() + if (!wasDisconnected && !didShowDisconnect()) { + success(`Connected to ${message.projectName}`) + } + // Process handshake through state machine await processEvent({ type: "HANDSHAKE", @@ -1078,16 +1092,6 @@ export async function start(config: Config): Promise { // Start file watcher now that we have a directory startWatcher() } - - // Cancel any pending disconnect message (fast reconnect) - cancelDisconnectMessage() - - // Only show "Connected" on initial connection, not reconnects - // Reconnect confirmation happens in SYNC_COMPLETE - const wasDisconnected = wasRecentlyDisconnected() - if (!wasDisconnected && !didShowDisconnect()) { - success(`Connected to ${message.projectName}`) - } })() }) diff --git a/plugins/code-link/README.md b/plugins/code-link/README.md index 30a27cbad..66228acbc 100644 --- a/plugins/code-link/README.md +++ b/plugins/code-link/README.md @@ -8,16 +8,16 @@ Two-way sync between Framer code components and your local filesystem. ## Features -- **Real-time two-way sync** — Edits in locally instantly appear in Framer, and vice-versa +- **Instant two-way sync** — Edits in locally instantly appear in Framer, and vice-versa - **Automatic types** — TypeScript types for `framer`, `framer-motion`, `react` are automatically installed - **Smart conflict resolution** — Auto-resolves when safe, prompts you to choose when both sides change -- **Project scaffolding** — Creates project files on first run; re-run with just `npx framer-code-link` -- **AI skill** — Installs Framer component best-practices for Cursor, Claude, and other AI editors +- **Zero config** — Creates config files on first run; re-run with just `npx framer-code-link` +- **AI skill** — Installs Framer component best-practices skill for Cursor, Claude, etc ## Quick Start 1. Open the **Code Link** Plugin in your Framer project -2. Copy the CLI command shown in the Plugin +2. Copy the CLI command from the Plugin, eg. `npx framer-code-link (hash)` 3. Paste and run the command in your terminal 4. Edit files in `{project}/files/` — changes sync to Framer From 559b36d12811cf62e98e600d7332c2263b4bec1c Mon Sep 17 00:00:00 2001 From: Hunter Caron Date: Thu, 12 Feb 2026 12:00:39 +0100 Subject: [PATCH 4/7] Publish --- packages/code-link-cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/code-link-cli/package.json b/packages/code-link-cli/package.json index f7e476f5e..4e3ef1ae2 100644 --- a/packages/code-link-cli/package.json +++ b/packages/code-link-cli/package.json @@ -1,6 +1,6 @@ { "name": "framer-code-link", - "version": "0.8.0", + "version": "0.10.0", "description": "CLI tool for syncing Framer code components - controller-centric architecture", "main": "dist/index.mjs", "type": "module", From 89b5ee49ee7f50b972649defcc7fe64764dfd17e Mon Sep 17 00:00:00 2001 From: Hunter Caron Date: Thu, 12 Feb 2026 12:21:59 +0100 Subject: [PATCH 5/7] Do not shorten directory to dir in names --- packages/code-link-cli/src/controller.ts | 19 ++++++---- packages/code-link-cli/src/index.ts | 2 +- packages/code-link-cli/src/types.ts | 2 +- packages/code-link-cli/src/utils/project.ts | 42 ++++++++++----------- 4 files changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/code-link-cli/src/controller.ts b/packages/code-link-cli/src/controller.ts index 4551f5586..34343f0d5 100644 --- a/packages/code-link-cli/src/controller.ts +++ b/packages/code-link-cli/src/controller.ts @@ -43,7 +43,7 @@ import { warn, wasRecentlyDisconnected, } from "./utils/logging.ts" -import { findOrCreateProjectDir } from "./utils/project.ts" +import { findOrCreateProjectDirectory } from "./utils/project.ts" import { hashFileContent } from "./utils/state-persistence.ts" /** @@ -682,8 +682,12 @@ async function executeEffect( if (!config.projectDir) { const projectName = config.explicitName ?? effect.projectInfo.projectName - const result = await findOrCreateProjectDir(config.projectHash, projectName, config.explicitDir) - config.projectDir = result.dir + const result = await findOrCreateProjectDirectory( + config.projectHash, + projectName, + config.explicitDirectory + ) + config.projectDir = result.directory config.projectDirCreated = result.created // May allow customization of file directory in the future @@ -953,7 +957,9 @@ async function executeEffect( return [] } - const relativeDir = config.projectDir ? "./" + (path.relative(process.cwd(), config.projectDir) || ".") : null + const relativeDir = config.projectDir + ? "./" + (path.relative(process.cwd(), config.projectDir) || ".") + : null if (effect.totalCount === 0 && relativeDir) { if (config.projectDirCreated) { @@ -962,9 +968,7 @@ async function executeEffect( success(`Syncing to ${relativeDir} folder`) } } else if (relativeDir && config.projectDirCreated) { - success( - `Synced into ${relativeDir} (${effect.updatedCount} files added)` - ) + success(`Synced into ${relativeDir} (${effect.updatedCount} files added)`) } else if (relativeDir) { success( `Synced into ${relativeDir} (${effect.updatedCount} files updated, ${effect.unchangedCount} unchanged)` @@ -1062,7 +1066,6 @@ export async function start(config: Config): Promise { } void (async () => { - // Cancel any pending disconnect message (fast reconnect) cancelDisconnectMessage() // Only show "Connected" on initial connection, not reconnects diff --git a/packages/code-link-cli/src/index.ts b/packages/code-link-cli/src/index.ts index 5c85f0c45..dc9ba839d 100644 --- a/packages/code-link-cli/src/index.ts +++ b/packages/code-link-cli/src/index.ts @@ -90,7 +90,7 @@ program filesDir: null, // Will be set during handshake dangerouslyAutoDelete: options.dangerouslyAutoDelete ?? false, allowUnsupportedNpm: options.unsupportedNpm ?? false, - explicitDir: options.dir, + explicitDirectory: options.dir, explicitName: options.name, } diff --git a/packages/code-link-cli/src/types.ts b/packages/code-link-cli/src/types.ts index 1a080ff99..92e967add 100644 --- a/packages/code-link-cli/src/types.ts +++ b/packages/code-link-cli/src/types.ts @@ -19,7 +19,7 @@ export interface Config { filesDir: string | null // Set during handshake , always projectDir/files dangerouslyAutoDelete: boolean allowUnsupportedNpm: boolean // Allow type acquisition for unsupported npm packages - explicitDir?: string // User-provided directory override + explicitDirectory?: string // User-provided directory override explicitName?: string // User-provided name override projectDirCreated?: boolean // Whether the project directory was newly created } diff --git a/packages/code-link-cli/src/utils/project.ts b/packages/code-link-cli/src/utils/project.ts index 044be2568..7f4ad31c4 100644 --- a/packages/code-link-cli/src/utils/project.ts +++ b/packages/code-link-cli/src/utils/project.ts @@ -18,7 +18,7 @@ export function toPackageName(name: string): string { .replace(/-+/g, "-") } -export function toDirName(name: string): string { +export function toDirectoryName(name: string): string { return name .replace(/[^a-zA-Z0-9- ]/g, "-") .replace(/^[-\s]+|[-\s]+$/g, "") @@ -36,33 +36,33 @@ export async function getProjectHashFromCwd(): Promise { } } -export async function findOrCreateProjectDir( +export async function findOrCreateProjectDirectory( projectHash: string, projectName?: string, - explicitDir?: string -): Promise<{ dir: string; created: boolean }> { - if (explicitDir) { - const resolved = path.resolve(explicitDir) + explicitDirectory?: string +): Promise<{ directory: string; created: boolean }> { + if (explicitDirectory) { + const resolved = path.resolve(explicitDirectory) await fs.mkdir(path.join(resolved, "files"), { recursive: true }) - return { dir: resolved, created: false } + return { directory: resolved, created: false } } const cwd = process.cwd() - const existing = await findExistingProjectDir(cwd, projectHash) + const existing = await findExistingProjectDirectory(cwd, projectHash) if (existing) { - return { dir: existing, created: false } + return { directory: existing, created: false } } if (!projectName) { throw new Error("Failed to get Project name. Pass --name .") } - const dirName = toDirName(projectName) + const directoryName = toDirectoryName(projectName) const pkgName = toPackageName(projectName) const shortId = shortProjectHash(projectHash) - const projectDir = path.join(cwd, dirName || shortId) + const projectDirectory = path.join(cwd, directoryName || shortId) - await fs.mkdir(path.join(projectDir, "files"), { recursive: true }) + await fs.mkdir(path.join(projectDirectory, "files"), { recursive: true }) const pkg: PackageJson = { name: pkgName || shortId, version: "1.0.0", @@ -70,23 +70,23 @@ export async function findOrCreateProjectDir( shortProjectHash: shortId, framerProjectName: projectName, } - await fs.writeFile(path.join(projectDir, "package.json"), JSON.stringify(pkg, null, 2)) + await fs.writeFile(path.join(projectDirectory, "package.json"), JSON.stringify(pkg, null, 2)) - return { dir: projectDir, created: true } + return { directory: projectDirectory, created: true } } -async function findExistingProjectDir(baseDir: string, projectHash: string): Promise { - const candidate = path.join(baseDir, "package.json") +async function findExistingProjectDirectory(baseDirectory: string, projectHash: string): Promise { + const candidate = path.join(baseDirectory, "package.json") if (await matchesProject(candidate, projectHash)) { - return baseDir + return baseDirectory } - const entries = await fs.readdir(baseDir, { withFileTypes: true }) + const entries = await fs.readdir(baseDirectory, { withFileTypes: true }) for (const entry of entries) { if (!entry.isDirectory()) continue - const dir = path.join(baseDir, entry.name) - if (await matchesProject(path.join(dir, "package.json"), projectHash)) { - return dir + const directory = path.join(baseDirectory, entry.name) + if (await matchesProject(path.join(directory, "package.json"), projectHash)) { + return directory } } From e7e96224761c1324e557157195e72ca758b8436b Mon Sep 17 00:00:00 2001 From: Hunter Caron Date: Thu, 12 Feb 2026 12:24:21 +0100 Subject: [PATCH 6/7] Rename variable --- packages/code-link-cli/src/controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/code-link-cli/src/controller.ts b/packages/code-link-cli/src/controller.ts index 34343f0d5..9b1f7cfc2 100644 --- a/packages/code-link-cli/src/controller.ts +++ b/packages/code-link-cli/src/controller.ts @@ -682,13 +682,13 @@ async function executeEffect( if (!config.projectDir) { const projectName = config.explicitName ?? effect.projectInfo.projectName - const result = await findOrCreateProjectDirectory( + const directoryInfo = await findOrCreateProjectDirectory( config.projectHash, projectName, config.explicitDirectory ) - config.projectDir = result.directory - config.projectDirCreated = result.created + config.projectDir = directoryInfo.directory + config.projectDirCreated = directoryInfo.created // May allow customization of file directory in the future config.filesDir = `${config.projectDir}/files` From 93c31641fde937e24b04fd7ee2e6b55de2d212f9 Mon Sep 17 00:00:00 2001 From: Hunter Caron Date: Thu, 12 Feb 2026 12:38:51 +0100 Subject: [PATCH 7/7] Fix log for running in cwd --- packages/code-link-cli/package.json | 2 +- packages/code-link-cli/src/controller.ts | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/code-link-cli/package.json b/packages/code-link-cli/package.json index 4e3ef1ae2..f2aa694ff 100644 --- a/packages/code-link-cli/package.json +++ b/packages/code-link-cli/package.json @@ -1,6 +1,6 @@ { "name": "framer-code-link", - "version": "0.10.0", + "version": "0.11.0", "description": "CLI tool for syncing Framer code components - controller-centric architecture", "main": "dist/index.mjs", "type": "module", diff --git a/packages/code-link-cli/src/controller.ts b/packages/code-link-cli/src/controller.ts index 9b1f7cfc2..1f0f68340 100644 --- a/packages/code-link-cli/src/controller.ts +++ b/packages/code-link-cli/src/controller.ts @@ -957,21 +957,20 @@ async function executeEffect( return [] } - const relativeDir = config.projectDir - ? "./" + (path.relative(process.cwd(), config.projectDir) || ".") - : null + const relative = config.projectDir ? path.relative(process.cwd(), config.projectDir) : null + const relativeDirectory = relative != null ? (relative ? "./" + relative : ".") : null - if (effect.totalCount === 0 && relativeDir) { + if (effect.totalCount === 0 && relativeDirectory) { if (config.projectDirCreated) { - success(`Created ${relativeDir} folder`) + success(`Created ${relativeDirectory} folder`) } else { - success(`Syncing to ${relativeDir} folder`) + success(`Syncing to ${relativeDirectory} folder`) } - } else if (relativeDir && config.projectDirCreated) { - success(`Synced into ${relativeDir} (${effect.updatedCount} files added)`) - } else if (relativeDir) { + } else if (relativeDirectory && config.projectDirCreated) { + success(`Synced into ${relativeDirectory} (${effect.updatedCount} files added)`) + } else if (relativeDirectory) { success( - `Synced into ${relativeDir} (${effect.updatedCount} files updated, ${effect.unchangedCount} unchanged)` + `Synced into ${relativeDirectory} (${effect.updatedCount} files updated, ${effect.unchangedCount} unchanged)` ) } else { success(