feat: add copySymlinks option and better default excludes for workspace sync

This commit is contained in:
Ash Brener 2026-01-28 15:01:38 +02:00
parent 38230547d6
commit 65bfb9f9d7
6 changed files with 30 additions and 1 deletions

View File

@ -162,6 +162,7 @@ export function registerWorkspaceCli(program: Command): void {
localPath: resolved.localPath, localPath: resolved.localPath,
conflictResolve: resolved.conflictResolve, conflictResolve: resolved.conflictResolve,
exclude: resolved.exclude, exclude: resolved.exclude,
copySymlinks: resolved.copySymlinks,
resync: opts.resync, resync: opts.resync,
dryRun: opts.dryRun, dryRun: opts.dryRun,
verbose: opts.verbose, verbose: opts.verbose,
@ -563,6 +564,7 @@ export function registerWorkspaceCli(program: Command): void {
localPath: resolved.localPath, localPath: resolved.localPath,
conflictResolve: resolved.conflictResolve, conflictResolve: resolved.conflictResolve,
exclude: resolved.exclude, exclude: resolved.exclude,
copySymlinks: resolved.copySymlinks,
resync: true, resync: true,
}); });

View File

@ -76,9 +76,17 @@ export type WorkspaceSyncConfig = {
/** /**
* File patterns to exclude from sync (glob patterns). * File patterns to exclude from sync (glob patterns).
* Defaults include: .git/**, node_modules/**, .venv/**, *.log, .DS_Store
*/ */
exclude?: string[]; 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"). * S3-specific configuration (when provider is "s3").
*/ */

View File

@ -552,6 +552,7 @@ export const MoltbotSchema = z
.union([z.literal("newer"), z.literal("local"), z.literal("remote")]) .union([z.literal("newer"), z.literal("local"), z.literal("remote")])
.optional(), .optional(),
exclude: z.array(z.string()).optional(), exclude: z.array(z.string()).optional(),
copySymlinks: z.boolean().optional(),
s3: z s3: z
.object({ .object({
endpoint: z.string().optional(), endpoint: z.string().optional(),

View File

@ -103,6 +103,7 @@ async function runSync(): Promise<void> {
localPath: resolved.localPath, localPath: resolved.localPath,
conflictResolve: resolved.conflictResolve, conflictResolve: resolved.conflictResolve,
exclude: resolved.exclude, exclude: resolved.exclude,
copySymlinks: resolved.copySymlinks,
resync: needsResync, resync: needsResync,
}); });
@ -116,6 +117,7 @@ async function runSync(): Promise<void> {
localPath: resolved.localPath, localPath: resolved.localPath,
conflictResolve: resolved.conflictResolve, conflictResolve: resolved.conflictResolve,
exclude: resolved.exclude, exclude: resolved.exclude,
copySymlinks: resolved.copySymlinks,
resync: true, resync: true,
}); });
} }

View File

@ -95,6 +95,7 @@ const workspaceSyncHandler: HookHandler = async (event) => {
localPath: resolved.localPath, localPath: resolved.localPath,
conflictResolve: resolved.conflictResolve, conflictResolve: resolved.conflictResolve,
exclude: resolved.exclude, exclude: resolved.exclude,
copySymlinks: resolved.copySymlinks,
verbose: false, verbose: false,
}); });

View File

@ -11,7 +11,14 @@ const DEFAULT_REMOTE_NAME = "cloud";
const DEFAULT_LOCAL_PATH = "shared"; const DEFAULT_LOCAL_PATH = "shared";
const DEFAULT_REMOTE_PATH = "moltbot-share"; const DEFAULT_REMOTE_PATH = "moltbot-share";
const DEFAULT_CONFLICT_RESOLVE = "newer"; 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 = { export type RcloneSyncResult = {
ok: boolean; ok: boolean;
@ -171,6 +178,7 @@ export function resolveSyncConfig(
configPath: string; configPath: string;
conflictResolve: "newer" | "local" | "remote"; conflictResolve: "newer" | "local" | "remote";
exclude: string[]; exclude: string[];
copySymlinks: boolean;
interval: number; interval: number;
onSessionStart: boolean; onSessionStart: boolean;
onSessionEnd: boolean; onSessionEnd: boolean;
@ -183,6 +191,7 @@ export function resolveSyncConfig(
configPath: config?.configPath ?? getDefaultRcloneConfigPath(stateDir), configPath: config?.configPath ?? getDefaultRcloneConfigPath(stateDir),
conflictResolve: config?.conflictResolve ?? DEFAULT_CONFLICT_RESOLVE, conflictResolve: config?.conflictResolve ?? DEFAULT_CONFLICT_RESOLVE,
exclude: config?.exclude ?? DEFAULT_EXCLUDES, exclude: config?.exclude ?? DEFAULT_EXCLUDES,
copySymlinks: config?.copySymlinks ?? false,
interval: config?.interval ?? 0, interval: config?.interval ?? 0,
onSessionStart: config?.onSessionStart ?? false, onSessionStart: config?.onSessionStart ?? false,
onSessionEnd: config?.onSessionEnd ?? false, onSessionEnd: config?.onSessionEnd ?? false,
@ -398,6 +407,7 @@ export async function runBisync(params: {
localPath: string; localPath: string;
conflictResolve: "newer" | "local" | "remote"; conflictResolve: "newer" | "local" | "remote";
exclude: string[]; exclude: string[];
copySymlinks?: boolean;
resync?: boolean; resync?: boolean;
dryRun?: boolean; dryRun?: boolean;
verbose?: boolean; verbose?: boolean;
@ -426,6 +436,11 @@ export async function runBisync(params: {
args.push("--exclude", pattern); args.push("--exclude", pattern);
} }
// Follow symlinks if configured
if (params.copySymlinks) {
args.push("--copy-links");
}
if (params.resync) { if (params.resync) {
args.push("--resync"); args.push("--resync");
} }