Skip to content

Commit e553807

Browse files
committed
feat: Add services gemini, codex, opencode, cursor
1 parent 50cb078 commit e553807

File tree

10 files changed

+158
-8
lines changed

10 files changed

+158
-8
lines changed

docs/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## v0.0.2
4+
5+
- Add built-in service definitions for Gemini (`~/.gemini`), Agents (`~/.agents`), OpenCode (`~/.config/opencode`), Codex (`~/.codex`), and Cursor (`~/.cursor`)
6+
37
## v0.0.1
48

59
- Central store as a git repo for AI config files with bidirectional sync using git-based 3-way merge

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ai-sync",
33
"author": "Anh-Thi Dinh",
4-
"version": "0.0.1",
4+
"version": "0.0.2",
55
"repository": "https://github.com/anhthiding/ai-sync",
66
"private": true,
77
"license": "MIT",

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@ai-sync/server",
33
"author": "Anh-Thi Dinh",
4-
"version": "0.0.1",
4+
"version": "0.0.2",
55
"repository": "https://github.com/anhthiding/ai-sync",
66
"private": true,
77
"license": "MIT",

packages/server/src/services/file-watcher.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export class FileWatcherService extends EventEmitter {
104104
ignoreInitial: true,
105105
awaitWriteFinish: { stabilityThreshold: 200 },
106106
ignored: ignorePatterns,
107+
followSymlinks: false,
107108
});
108109

109110
watcher.on('all', (event, filePath) => {
@@ -163,6 +164,7 @@ export class FileWatcherService extends EventEmitter {
163164
ignoreInitial: true,
164165
awaitWriteFinish: { stabilityThreshold: 200 },
165166
ignored: ['.DS_Store', '**/.DS_Store', ...ignorePatterns],
167+
followSymlinks: false,
166168
});
167169

168170
watcher.on('all', (event, filePath) => {

packages/server/src/services/service-definitions.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,36 @@ export const SERVICE_DEFINITIONS: Record<string, ServiceDefinition> = {
2222
'scripts/**',
2323
],
2424
},
25+
gemini: {
26+
serviceType: 'gemini',
27+
name: 'Gemini',
28+
defaultPath: path.join(os.homedir(), '.gemini'),
29+
patterns: ['GEMINI.md', 'settings.json', 'skills/**'],
30+
},
31+
agents: {
32+
serviceType: 'agents',
33+
name: 'Agents',
34+
defaultPath: path.join(os.homedir(), '.agents'),
35+
patterns: ['skills/**'],
36+
},
37+
opencode: {
38+
serviceType: 'opencode',
39+
name: 'OpenCode',
40+
defaultPath: path.join(os.homedir(), '.config', 'opencode'),
41+
patterns: ['skills/**'],
42+
},
43+
cursor: {
44+
serviceType: 'cursor',
45+
name: 'Cursor',
46+
defaultPath: path.join(os.homedir(), '.cursor'),
47+
patterns: ['skills/**', 'mcp.json'],
48+
},
49+
codex: {
50+
serviceType: 'codex',
51+
name: 'Codex',
52+
defaultPath: path.join(os.homedir(), '.codex'),
53+
patterns: ['skills/**'],
54+
},
2555
};
2656

2757
// Runtime registry for custom service definitions (populated from DB on startup)

packages/server/src/services/service-scanner.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import type { ScannedEntry } from './repo-scanner.js';
66
/**
77
* Scan a service directory for files matching the given patterns.
88
* Optionally accepts ignore patterns to exclude files from the results.
9+
*
10+
* Symlinks are handled the same way as in repo scanning:
11+
* - Regular files whose parent path traverses a symlink are excluded
12+
* - Root segments that are themselves symlinks are tracked as symlink entries
913
*/
1014
export async function scanServiceFiles(
1115
servicePath: string,
@@ -17,7 +21,16 @@ export async function scanServiceFiles(
1721

1822
const ignore = ['.DS_Store', '**/.DS_Store', ...ignorePatterns];
1923

24+
// Collect root segments from patterns to check for symlinks
25+
const rootSegmentsToCheck = new Set<string>();
26+
2027
for (const pattern of patterns) {
28+
const firstSlash = pattern.indexOf('/');
29+
const rootSegment = firstSlash === -1 ? pattern : pattern.substring(0, firstSlash);
30+
if (!rootSegment.includes('*') && !rootSegment.includes('?')) {
31+
rootSegmentsToCheck.add(rootSegment);
32+
}
33+
2134
const matches = await glob(pattern, {
2235
cwd: servicePath,
2336
nodir: true,
@@ -43,5 +56,16 @@ export async function scanServiceFiles(
4356
}
4457
}
4558

59+
// Check root segments for symlinks (e.g. "skills" itself might be a symlink)
60+
for (const segment of rootSegmentsToCheck) {
61+
const fullPath = path.join(servicePath, segment);
62+
if (await isSymlink(fullPath)) {
63+
if (!seenPaths.has(segment)) {
64+
seenPaths.add(segment);
65+
filtered.push({ path: segment, isSymlink: true });
66+
}
67+
}
68+
}
69+
4670
return filtered.sort((a, b) => a.path.localeCompare(b.path));
4771
}

packages/server/src/services/sync-engine.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,6 +1334,21 @@ export class SyncEngine {
13341334

13351335
let newCount = 0;
13361336

1337+
// Clean up tracked files whose parent path goes through a symlink
1338+
for (const e of existing) {
1339+
if (
1340+
e.file_type !== 'symlink' &&
1341+
(await parentPathHasSymlink(svc.localPath, e.relative_path))
1342+
) {
1343+
this.db
1344+
.prepare(
1345+
'DELETE FROM tracked_files WHERE service_config_id = ? AND relative_path = ?',
1346+
)
1347+
.run(svc.id, e.relative_path);
1348+
existingPaths.delete(e.relative_path);
1349+
}
1350+
}
1351+
13371352
// Scan target service folder for new files
13381353
const patterns = getServiceEnabledPatterns(this.db, svc.id, def.patterns);
13391354
const svcIgnorePatterns = expandIgnorePatterns(

packages/ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@ai-sync/ui",
33
"author": "Anh-Thi Dinh",
4-
"version": "0.0.1",
4+
"version": "0.0.2",
55
"repository": "https://github.com/anhthiding/ai-sync",
66
"private": true,
77
"license": "MIT",

packages/ui/src/components/add-service-dialog.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ export function AddServiceDialog({ open, onOpenChange, onAdded }: AddServiceDial
4141
try {
4242
await api.services.create(serviceType);
4343
onAdded();
44-
onOpenChange(false);
44+
setAvailable((prev) =>
45+
prev.map((s) => (s.serviceType === serviceType ? { ...s, registered: true } : s)),
46+
);
4547
} catch {
4648
// Error handled by api client
4749
} finally {
@@ -87,11 +89,17 @@ export function AddServiceDialog({ open, onOpenChange, onAdded }: AddServiceDial
8789
<div className="flex items-center gap-2 font-medium text-sm">
8890
<span>{svc.name}</span>
8991
{svc.serviceType.startsWith('custom-') ? (
90-
<Badge variant="secondary">Custom</Badge>
92+
<Badge variant="secondary" className="px-1.5 py-0 text-[10px]">
93+
Custom
94+
</Badge>
9195
) : svc.detected ? (
92-
<Badge variant="success">Detected</Badge>
96+
<Badge variant="success" className="px-1.5 py-0 text-[10px]">
97+
Detected
98+
</Badge>
9399
) : (
94-
<Badge variant="outline">Not detected</Badge>
100+
<Badge variant="outline" className="px-1.5 py-0 text-[10px]">
101+
Not detected
102+
</Badge>
95103
)}
96104
</div>
97105
<div className="text-xs text-muted-foreground font-mono truncate">

packages/ui/src/components/service-icon.tsx

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Terminal } from 'lucide-react';
1+
import { Terminal, Bot } from 'lucide-react';
22

33
function ClaudeLogo({ className }: { className?: string }) {
44
return (
@@ -13,6 +13,63 @@ function ClaudeLogo({ className }: { className?: string }) {
1313
);
1414
}
1515

16+
function GeminiLogo({ className }: { className?: string }) {
17+
return (
18+
<svg
19+
xmlns="http://www.w3.org/2000/svg"
20+
viewBox="0 0 16 16"
21+
fill="currentColor"
22+
className={className}
23+
>
24+
<path
25+
d="M16 8a8.001 8.001 0 0 1-8 8 8.001 8.001 0 0 1-8-8 8.001 8.001 0 0 1 8-8 8.001 8.001 0 0 1 8 8z"
26+
fillOpacity="0"
27+
/>
28+
<path d="M16 8c-3.314 0-5.686-1.343-7.029-3.343C7.93 3.002 8 1.537 8 0c0 1.537.07 3.002 1.029 4.657C10.343 6.657 12.686 8 16 8zM0 8c3.314 0 5.686 1.343 7.029 3.343C8.07 12.998 8 14.463 8 16c0-1.537-.07-3.002-1.029-4.657C5.657 9.343 3.314 8 0 8z" />
29+
<path d="M8 16c0-3.314 1.343-5.686 3.343-7.029C12.998 7.93 14.463 8 16 8c-1.537 0-3.002-.07-4.657-1.029C9.343 5.657 8 3.314 8 0c0 3.314-1.343 5.686-3.343 7.029C3.002 8.07 1.537 8 0 8c1.537 0 3.002.07 4.657 1.029C6.657 10.343 8 12.686 8 16z" />
30+
</svg>
31+
);
32+
}
33+
34+
function CursorLogo({ className }: { className?: string }) {
35+
return (
36+
<svg
37+
xmlns="http://www.w3.org/2000/svg"
38+
viewBox="0 0 24 24"
39+
fill="currentColor"
40+
className={className}
41+
>
42+
<path d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23" />
43+
</svg>
44+
);
45+
}
46+
47+
function CodexLogo({ className }: { className?: string }) {
48+
return (
49+
<svg
50+
xmlns="http://www.w3.org/2000/svg"
51+
viewBox="0 0 24 24"
52+
fill="currentColor"
53+
className={className}
54+
>
55+
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
56+
</svg>
57+
);
58+
}
59+
60+
function OpenCodeLogo({ className }: { className?: string }) {
61+
return (
62+
<svg
63+
xmlns="http://www.w3.org/2000/svg"
64+
viewBox="0 0 16 16"
65+
fill="currentColor"
66+
className={className}
67+
>
68+
<path fillRule="evenodd" d="M12 13H4V3h8v10zM10 5H6v6h4V5z" />
69+
</svg>
70+
);
71+
}
72+
1673
interface ServiceIconProps {
1774
serviceType: string;
1875
serviceId?: string;
@@ -35,6 +92,16 @@ export function ServiceIcon({ serviceType, serviceId, iconPath, className }: Ser
3592
switch (serviceType) {
3693
case 'claude-code':
3794
return <ClaudeLogo className={className} />;
95+
case 'gemini':
96+
return <GeminiLogo className={className} />;
97+
case 'cursor':
98+
return <CursorLogo className={className} />;
99+
case 'agents':
100+
return <Bot className={className} />;
101+
case 'opencode':
102+
return <OpenCodeLogo className={className} />;
103+
case 'codex':
104+
return <CodexLogo className={className} />;
38105
default:
39106
return <Terminal className={className} />;
40107
}

0 commit comments

Comments
 (0)