diff --git a/packages/code-link-cli/package.json b/packages/code-link-cli/package.json index ce92fb316..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.7.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 03eac1002..1f0f68340 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 { @@ -42,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" /** @@ -681,7 +682,13 @@ async function executeEffect( if (!config.projectDir) { const projectName = config.explicitName ?? effect.projectInfo.projectName - config.projectDir = await findOrCreateProjectDir(config.projectHash, projectName, config.explicitDir) + const directoryInfo = await findOrCreateProjectDirectory( + config.projectHash, + projectName, + config.explicitDirectory + ) + config.projectDir = directoryInfo.directory + config.projectDirCreated = directoryInfo.created // May allow customization of file directory in the future config.filesDir = `${config.projectDir}/files` @@ -950,9 +957,26 @@ async function executeEffect( return [] } - success( - `Synced ${effect.totalCount} files (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)` - ) + const relative = config.projectDir ? path.relative(process.cwd(), config.projectDir) : null + const relativeDirectory = relative != null ? (relative ? "./" + relative : ".") : null + + if (effect.totalCount === 0 && relativeDirectory) { + if (config.projectDirCreated) { + success(`Created ${relativeDirectory} folder`) + } else { + success(`Syncing to ${relativeDirectory} folder`) + } + } else if (relativeDirectory && config.projectDirCreated) { + success(`Synced into ${relativeDirectory} (${effect.updatedCount} files added)`) + } else if (relativeDirectory) { + success( + `Synced into ${relativeDirectory} (${effect.updatedCount} files updated, ${effect.unchangedCount} unchanged)` + ) + } else { + success( + `Synced ${effect.totalCount} files (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)` + ) + } status("Watching for changes...") return [] } @@ -1041,6 +1065,15 @@ export async function start(config: Config): Promise { } void (async () => { + 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", @@ -1061,16 +1094,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/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 4283906ad..92e967add 100644 --- a/packages/code-link-cli/src/types.ts +++ b/packages/code-link-cli/src/types.ts @@ -19,8 +19,9 @@ 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 } // 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..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 { - 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 resolved + return { directory: resolved, created: false } } const cwd = process.cwd() - const existing = await findExistingProjectDir(cwd, projectHash) + const existing = await findExistingProjectDirectory(cwd, projectHash) if (existing) { - return existing + 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 projectDir + 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 } } diff --git a/plugins/code-link/README.md b/plugins/code-link/README.md index d842fb2ce..66228acbc 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 -2. Copy the CLI command shown in the Plugin -3. Run the command in your terminal to start syncing +- **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 +- **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 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 + +## 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