diff --git a/src/cli/workspace-cli.ts b/src/cli/workspace-cli.ts index 42cf89ec2..538d77a8a 100644 --- a/src/cli/workspace-cli.ts +++ b/src/cli/workspace-cli.ts @@ -162,6 +162,7 @@ export function registerWorkspaceCli(program: Command): void { localPath: resolved.localPath, conflictResolve: resolved.conflictResolve, exclude: resolved.exclude, + copySymlinks: resolved.copySymlinks, resync: opts.resync, dryRun: opts.dryRun, verbose: opts.verbose, @@ -563,6 +564,7 @@ export function registerWorkspaceCli(program: Command): void { localPath: resolved.localPath, conflictResolve: resolved.conflictResolve, exclude: resolved.exclude, + copySymlinks: resolved.copySymlinks, resync: true, }); diff --git a/src/config/types.workspace.ts b/src/config/types.workspace.ts index 0b486ce3a..de1feb6ff 100644 --- a/src/config/types.workspace.ts +++ b/src/config/types.workspace.ts @@ -76,9 +76,17 @@ export type WorkspaceSyncConfig = { /** * File patterns to exclude from sync (glob patterns). + * Defaults include: .git/**, node_modules/**, .venv/**, *.log, .DS_Store */ exclude?: string[]; + /** + * Follow symlinks during sync (default: false). + * When false, symlinks are skipped with a notice. + * When true, symlinks are followed and their targets are copied. + */ + copySymlinks?: boolean; + /** * S3-specific configuration (when provider is "s3"). */ diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index f2686c93d..33800f38e 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -552,6 +552,7 @@ export const MoltbotSchema = z .union([z.literal("newer"), z.literal("local"), z.literal("remote")]) .optional(), exclude: z.array(z.string()).optional(), + copySymlinks: z.boolean().optional(), s3: z .object({ endpoint: z.string().optional(), diff --git a/src/gateway/workspace-sync-manager.ts b/src/gateway/workspace-sync-manager.ts index d08e60c7e..15a36c916 100644 --- a/src/gateway/workspace-sync-manager.ts +++ b/src/gateway/workspace-sync-manager.ts @@ -103,6 +103,7 @@ async function runSync(): Promise { localPath: resolved.localPath, conflictResolve: resolved.conflictResolve, exclude: resolved.exclude, + copySymlinks: resolved.copySymlinks, resync: needsResync, }); @@ -116,6 +117,7 @@ async function runSync(): Promise { localPath: resolved.localPath, conflictResolve: resolved.conflictResolve, exclude: resolved.exclude, + copySymlinks: resolved.copySymlinks, resync: true, }); } diff --git a/src/hooks/bundled/workspace-sync/handler.ts b/src/hooks/bundled/workspace-sync/handler.ts index 30bdfe734..8a2c5396b 100644 --- a/src/hooks/bundled/workspace-sync/handler.ts +++ b/src/hooks/bundled/workspace-sync/handler.ts @@ -95,6 +95,7 @@ const workspaceSyncHandler: HookHandler = async (event) => { localPath: resolved.localPath, conflictResolve: resolved.conflictResolve, exclude: resolved.exclude, + copySymlinks: resolved.copySymlinks, verbose: false, }); diff --git a/src/infra/rclone.ts b/src/infra/rclone.ts index bfa493516..09e907ad1 100644 --- a/src/infra/rclone.ts +++ b/src/infra/rclone.ts @@ -11,7 +11,14 @@ const DEFAULT_REMOTE_NAME = "cloud"; const DEFAULT_LOCAL_PATH = "shared"; const DEFAULT_REMOTE_PATH = "moltbot-share"; const DEFAULT_CONFLICT_RESOLVE = "newer"; -const DEFAULT_EXCLUDES = [".git/**", "node_modules/**", "*.log", ".DS_Store"]; +const DEFAULT_EXCLUDES = [ + ".git/**", + "node_modules/**", + ".venv/**", + "__pycache__/**", + "*.log", + ".DS_Store", +]; export type RcloneSyncResult = { ok: boolean; @@ -171,6 +178,7 @@ export function resolveSyncConfig( configPath: string; conflictResolve: "newer" | "local" | "remote"; exclude: string[]; + copySymlinks: boolean; interval: number; onSessionStart: boolean; onSessionEnd: boolean; @@ -183,6 +191,7 @@ export function resolveSyncConfig( configPath: config?.configPath ?? getDefaultRcloneConfigPath(stateDir), conflictResolve: config?.conflictResolve ?? DEFAULT_CONFLICT_RESOLVE, exclude: config?.exclude ?? DEFAULT_EXCLUDES, + copySymlinks: config?.copySymlinks ?? false, interval: config?.interval ?? 0, onSessionStart: config?.onSessionStart ?? false, onSessionEnd: config?.onSessionEnd ?? false, @@ -398,6 +407,7 @@ export async function runBisync(params: { localPath: string; conflictResolve: "newer" | "local" | "remote"; exclude: string[]; + copySymlinks?: boolean; resync?: boolean; dryRun?: boolean; verbose?: boolean; @@ -426,6 +436,11 @@ export async function runBisync(params: { args.push("--exclude", pattern); } + // Follow symlinks if configured + if (params.copySymlinks) { + args.push("--copy-links"); + } + if (params.resync) { args.push("--resync"); }