Skip to content

Commit cfb30ad

Browse files
authored
Skip redundant sync when configs already linked (#4)
1 parent d7a124e commit cfb30ad

20 files changed

Lines changed: 501 additions & 103 deletions

package.json

Lines changed: 66 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,68 @@
11
{
2-
"name": "@donnes/syncode",
3-
"version": "1.1.7",
4-
"description": "Sync AI code agent configs (Claude Code, Cursor, Windsurf, OpenCode) across machines and projects.",
5-
"private": false,
6-
"type": "module",
7-
"bin": {
8-
"syncode": "./dist/index.js"
9-
},
10-
"main": "./dist/index.js",
11-
"types": "./dist/index.d.ts",
12-
"files": [
13-
"dist",
14-
"README.md",
15-
"LICENSE"
16-
],
17-
"scripts": {
18-
"build": "tsup",
19-
"dev": "tsx src/index.ts",
20-
"test": "tsx scripts/execute-tests.ts",
21-
"prepublishOnly": "npm run build",
22-
"check:lint": "biome check . --diagnostic-level=error",
23-
"check:unsafe": "biome check . --write --unsafe --diagnostic-level=error",
24-
"check:types": "bunx tsc --noEmit"
25-
},
26-
"keywords": [
27-
"ai-agents",
28-
"claude-code",
29-
"cursor",
30-
"windsurf",
31-
"opencode",
32-
"vscode",
33-
"ai-coding",
34-
"code-agent",
35-
"skill-conversion",
36-
"syncode",
37-
"dotfiles",
38-
"configuration-management",
39-
"developer-tools",
40-
"ai-assistant",
41-
"coding-assistant"
42-
],
43-
"engines": {
44-
"node": ">=20.0.0"
45-
},
46-
"repository": {
47-
"type": "git",
48-
"url": "git+https://github.com/donnes/syncode.git"
49-
},
50-
"bugs": {
51-
"url": "https://github.com/donnes/syncode/issues"
52-
},
53-
"homepage": "https://github.com/donnes/syncode#readme",
54-
"author": "Donald Silveira",
55-
"license": "MIT",
56-
"dependencies": {
57-
"@clack/prompts": "^0.9.1"
58-
},
59-
"devDependencies": {
60-
"@biomejs/biome": "^2.3.11",
61-
"@opencode-ai/sdk": "^1.1.41",
62-
"@types/bun": "latest",
63-
"@types/node": "^20.0.0",
64-
"tsup": "^8.5.1",
65-
"tsx": "^4.21.0",
66-
"typescript": "^5.7.3"
67-
}
2+
"name": "@donnes/syncode",
3+
"version": "1.1.7",
4+
"description": "Sync AI code agent configs (Claude Code, Cursor, Windsurf, OpenCode) across machines and projects.",
5+
"private": false,
6+
"type": "module",
7+
"bin": {
8+
"syncode": "./dist/index.js"
9+
},
10+
"main": "./dist/index.js",
11+
"types": "./dist/index.d.ts",
12+
"files": [
13+
"dist",
14+
"README.md",
15+
"LICENSE"
16+
],
17+
"scripts": {
18+
"build": "tsup",
19+
"dev": "tsx src/index.ts",
20+
"test": "tsx scripts/execute-tests.ts",
21+
"prepublishOnly": "npm run build",
22+
"check:lint": "biome check . --diagnostic-level=error",
23+
"check:unsafe": "biome check . --write --unsafe --diagnostic-level=error",
24+
"check:types": "bunx tsc --noEmit"
25+
},
26+
"keywords": [
27+
"ai-agents",
28+
"claude-code",
29+
"cursor",
30+
"windsurf",
31+
"opencode",
32+
"vscode",
33+
"ai-coding",
34+
"code-agent",
35+
"skill-conversion",
36+
"syncode",
37+
"dotfiles",
38+
"configuration-management",
39+
"developer-tools",
40+
"ai-assistant",
41+
"coding-assistant"
42+
],
43+
"engines": {
44+
"node": ">=20.0.0"
45+
},
46+
"repository": {
47+
"type": "git",
48+
"url": "git+https://github.com/donnes/syncode.git"
49+
},
50+
"bugs": {
51+
"url": "https://github.com/donnes/syncode/issues"
52+
},
53+
"homepage": "https://github.com/donnes/syncode#readme",
54+
"author": "Donald Silveira",
55+
"license": "MIT",
56+
"dependencies": {
57+
"@clack/prompts": "^0.9.1"
58+
},
59+
"devDependencies": {
60+
"@biomejs/biome": "^2.3.11",
61+
"@opencode-ai/sdk": "^1.1.41",
62+
"@types/bun": "latest",
63+
"@types/node": "^20.0.0",
64+
"tsup": "^8.5.1",
65+
"tsx": "^4.21.0",
66+
"typescript": "^5.7.3"
67+
}
6868
}

scripts/release.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pkg.version = newVersion;
5252
await writeFile("package.json", `${JSON.stringify(pkg, null, 2)}\n`);
5353
console.log("Updated package.json");
5454

55+
await $`bun check:unsafe`;
56+
5557
// 5. Git operations
5658
const tag = `v${newVersion}`;
5759
let output = `version=${newVersion}\ntag=${tag}\n`;

src/adapters/amp.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
createSymlink,
1010
ensureDir,
1111
exists,
12+
getSymlinkTarget,
1213
isSymlink,
1314
removeDir,
1415
} from "../utils/fs";
@@ -52,7 +53,10 @@ export class AmpAdapter implements AgentAdapter {
5253
}
5354

5455
isLinked(systemPath: string, repoPath: string): boolean {
55-
return exists(systemPath) && exists(repoPath) && isSymlink(systemPath);
56+
if (!exists(systemPath) || !exists(repoPath) || !isSymlink(systemPath)) {
57+
return false;
58+
}
59+
return getSymlinkTarget(systemPath) === repoPath;
5660
}
5761

5862
async import(systemPath: string, repoPath: string): Promise<ImportResult> {
@@ -63,6 +67,20 @@ export class AmpAdapter implements AgentAdapter {
6367
};
6468
}
6569

70+
if (isSymlink(systemPath)) {
71+
return {
72+
success: true,
73+
message: "Already linked to repo - no import needed",
74+
};
75+
}
76+
77+
if (exists(repoPath)) {
78+
return {
79+
success: true,
80+
message: "Configs already in repo - no import needed",
81+
};
82+
}
83+
6684
ensureDir(repoPath);
6785
copyDir(systemPath, repoPath);
6886

@@ -80,6 +98,17 @@ export class AmpAdapter implements AgentAdapter {
8098
};
8199
}
82100

101+
if (isSymlink(systemPath)) {
102+
const target = getSymlinkTarget(systemPath);
103+
if (target === repoPath) {
104+
return {
105+
success: true,
106+
message: "Already linked to repo - no export needed",
107+
linkedTo: repoPath,
108+
};
109+
}
110+
}
111+
83112
// Remove existing (symlink or directory)
84113
if (exists(systemPath)) {
85114
if (isSymlink(systemPath)) {

src/adapters/antigravity.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
createSymlink,
1010
ensureDir,
1111
exists,
12+
getSymlinkTarget,
1213
isSymlink,
1314
removeDir,
1415
} from "../utils/fs";
@@ -54,7 +55,10 @@ export class AntigravityAdapter implements AgentAdapter {
5455
}
5556

5657
isLinked(systemPath: string, repoPath: string): boolean {
57-
return exists(systemPath) && exists(repoPath) && isSymlink(systemPath);
58+
if (!exists(systemPath) || !exists(repoPath) || !isSymlink(systemPath)) {
59+
return false;
60+
}
61+
return getSymlinkTarget(systemPath) === repoPath;
5862
}
5963

6064
async import(systemPath: string, repoPath: string): Promise<ImportResult> {
@@ -65,6 +69,20 @@ export class AntigravityAdapter implements AgentAdapter {
6569
};
6670
}
6771

72+
if (isSymlink(systemPath)) {
73+
return {
74+
success: true,
75+
message: "Already linked to repo - no import needed",
76+
};
77+
}
78+
79+
if (exists(repoPath)) {
80+
return {
81+
success: true,
82+
message: "Configs already in repo - no import needed",
83+
};
84+
}
85+
6886
ensureDir(repoPath);
6987
copyDir(systemPath, repoPath);
7088

@@ -82,6 +100,17 @@ export class AntigravityAdapter implements AgentAdapter {
82100
};
83101
}
84102

103+
if (isSymlink(systemPath)) {
104+
const target = getSymlinkTarget(systemPath);
105+
if (target === repoPath) {
106+
return {
107+
success: true,
108+
message: "Already linked to repo - no export needed",
109+
linkedTo: repoPath,
110+
};
111+
}
112+
}
113+
85114
// Remove existing (symlink or directory)
86115
if (exists(systemPath)) {
87116
if (isSymlink(systemPath)) {

src/adapters/claude.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
* Claude Code adapter
33
*/
44

5-
import { copyFileSync, existsSync, statSync, unlinkSync } from "node:fs";
5+
import { copyFileSync, existsSync, statSync } from "node:fs";
66
import { dirname, join } from "node:path";
77
import {
88
copyDir,
99
ensureDir,
1010
exists,
1111
isDirectory,
12-
removeDir,
12+
isSymlink,
1313
} from "../utils/fs";
1414
import { contractHome } from "../utils/paths";
1515
import type {
@@ -89,6 +89,10 @@ export class ClaudeAdapter implements AgentAdapter {
8989

9090
if (!existsSync(srcPath)) continue;
9191

92+
if (isSymlink(srcPath)) continue;
93+
94+
if (existsSync(destPath)) continue;
95+
9296
if (statSync(srcPath).isDirectory()) {
9397
copyDir(srcPath, destPath);
9498
filesImported.push(`${pattern}/`);
@@ -134,14 +138,7 @@ export class ClaudeAdapter implements AgentAdapter {
134138

135139
if (!existsSync(srcPath)) continue;
136140

137-
// Remove existing and copy fresh
138-
if (existsSync(destPath)) {
139-
if (statSync(destPath).isDirectory()) {
140-
removeDir(destPath);
141-
} else {
142-
unlinkSync(destPath);
143-
}
144-
}
141+
if (existsSync(destPath)) continue;
145142

146143
if (statSync(srcPath).isDirectory()) {
147144
copyDir(srcPath, destPath);

src/adapters/clawdbot.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
createSymlink,
1010
ensureDir,
1111
exists,
12+
getSymlinkTarget,
1213
isSymlink,
1314
removeDir,
1415
} from "../utils/fs";
@@ -62,7 +63,10 @@ export class ClawdbotAdapter implements AgentAdapter {
6263
}
6364

6465
isLinked(systemPath: string, repoPath: string): boolean {
65-
return exists(systemPath) && exists(repoPath) && isSymlink(systemPath);
66+
if (!exists(systemPath) || !exists(repoPath) || !isSymlink(systemPath)) {
67+
return false;
68+
}
69+
return getSymlinkTarget(systemPath) === repoPath;
6670
}
6771

6872
async import(systemPath: string, repoPath: string): Promise<ImportResult> {
@@ -73,6 +77,20 @@ export class ClawdbotAdapter implements AgentAdapter {
7377
};
7478
}
7579

80+
if (isSymlink(systemPath)) {
81+
return {
82+
success: true,
83+
message: "Already linked to repo - no import needed",
84+
};
85+
}
86+
87+
if (exists(repoPath)) {
88+
return {
89+
success: true,
90+
message: "Configs already in repo - no import needed",
91+
};
92+
}
93+
7694
ensureDir(repoPath);
7795
copyDir(systemPath, repoPath);
7896

@@ -110,6 +128,17 @@ export class ClawdbotAdapter implements AgentAdapter {
110128
repoPath: string,
111129
systemPath: string,
112130
): Promise<ExportResult> {
131+
if (isSymlink(systemPath)) {
132+
const target = getSymlinkTarget(systemPath);
133+
if (target === repoPath) {
134+
return {
135+
success: true,
136+
message: "Already linked to repo - no export needed",
137+
linkedTo: repoPath,
138+
};
139+
}
140+
}
141+
113142
if (exists(systemPath) && !isSymlink(systemPath)) {
114143
const backupPath = `${systemPath}.backup`;
115144
if (exists(backupPath)) {

0 commit comments

Comments
 (0)