Skip to content

Commit e9864d6

Browse files
committed
feat: add custom service
1 parent f9761df commit e9864d6

21 files changed

+830
-177
lines changed

docs/changelog.md

Lines changed: 41 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,80 @@
11
# Changelog
22

3+
## v1.7.0 - Custom service support
4+
5+
- Custom AI service management: add services with name, description, icon, config path, and file patterns
6+
- DB migration: add `description` and `icon_path` to `service_configs`
7+
38
## v1.6.0 - Multi-machine support
49

5-
- **New:** Multi-machine support — each machine gets a unique ID and name, stored in local config (`~/.ai-sync/config.json`)
6-
- **New:** Git-tracked `machines.json` in the store repo maps repo/service paths per machine, enabling cross-machine sync with different absolute paths
7-
- **New:** Auto-link on startup — repos with valid path mappings for the current machine are automatically registered in the local database
8-
- **New:** Unlinked repos detection — dashboard shows store repos that exist but aren't linked on the current machine, with manual link and auto-link options
9-
- **New:** Machine name displayed in the footer alongside the data directory
10-
- **New:** Settings "Machine" tab — edit machine name, view machine ID, see all known machines
10+
- Multi-machine support with unique machine ID/name per device
11+
- Git-tracked `machines.json` maps repo/service paths per machine for cross-machine sync
12+
- Auto-link repos on startup; dashboard shows unlinked repos with link options
1113

1214
## v1.5.1
1315

14-
- **New:** Right-click "Delete" on file tree — deletes files/folders from both store and target repo, with empty parent directory cleanup
15-
- **New:** Size labels: normal (<1 MB), amber (1–10 MB), red (>10 MB), violet (>50 MB)
16-
- **Perf:** code-splitting with manual chunks (React, CodeMirror, Radix, Markdown) and lazy-loaded routes
17-
- **Perf:** removed Framer Motion (~150 KB) — replaced with CSS transitions
18-
- **New:** Build a single commit message from a batch of messages
19-
- **New:** "Apply .gitignore" button in repo settings and global settings — re-applies managed `.gitignore` block and untracks files from git (useful after cloning the data repo)
20-
- **New:** Search button in pattern lists — filter patterns by keyword
16+
- Right-click "Delete" on file tree (removes from both store and target)
17+
- File size labels: normal (<1 MB), amber (1–10 MB), red (>10 MB), violet (>50 MB)
18+
- Batch commit message builder
19+
- "Apply .gitignore" button in repo/global settings
20+
- Search in pattern lists
21+
- Code-splitting, lazy-loaded routes, removed Framer Motion
2122

22-
## v1.5.0 - Breaking change - Removed desktop apps
23+
## v1.5.0 - Removed desktop app
2324

24-
- Removed desktop app (Tauri v2) — the project now runs as a web app only (`pnpm build && pnpm start`)
25-
- Removed sidecar build pipeline, Tauri CORS origins, and all desktop-related code
26-
- Simplified UI API client and WebSocket client (no more port-based routing for Tauri)
25+
- Removed Tauri v2 desktop app — web app only (`pnpm build && pnpm start`)
2726

2827
## v1.4.2
2928

30-
- **Fix:** `FOREIGN KEY constraint failed` error when syncing services`sync_log.repo_id` had a foreign key referencing `repos(id)` but services write `service_config_id` into that column
29+
- Fix: `FOREIGN KEY constraint failed` when syncing services
3130

3231
## v1.4.1
3332

34-
- **Fix:** `.gitignore` managed block now syncs with file pattern settings — adding/removing/disabling patterns updates all active repos' `.gitignore` files automatically
35-
- **Fix:** false conflicts in service sync (e.g. `~/.claude/`) caused by uncommitted store git changes
36-
- **Fix:** Adding ignore patterns now takes effect immediately — matching tracked files are untracked and removed from store on save
37-
- **Fix:** Repo watcher now restarts when ignore patterns change
38-
- **New:** Search files in file tree
39-
- **New:** Ignore patterns for services — override global ignore patterns or add custom ones per service (same as repos)
40-
- **New:** Right-click context menu on file tree — "Ignore this file/folder" adds the pattern to ignore list and immediately untracks matching files
41-
- **New:** toggle button to show largest files in file tree
42-
- **Security:** path traversal protection (`safeJoin`) on all file routes — prevents `../` escape from store/repo directories
43-
- **Security:** command injection fix in open-folder — replaced shell-spawning `exec` with `execFile`
44-
- **Security:** CORS restricted to `localhost` / `127.0.0.1` only (was `origin: true`)
45-
- **Security:** default listen host changed from `0.0.0.0` to `127.0.0.1`
46-
- **Security:** symlink target validation — rejects absolute paths and `..` escaping
47-
- **Security:** WebSocket `maxPayload` limit (1 MB)
48-
- **Security:** sync_log auto-cleanup (entries older than 30 days removed on startup)
49-
- **Perf:** N+1 query fix in repo/service listing — single aggregating SQL instead of per-item queries
50-
- **Perf:** polling `setInterval` replaced with `setTimeout` chain — prevents overlapping sync cycles
51-
- **Perf:** `picomatch` compiled matchers cached per pattern set
52-
- **Perf:** periodic cleanup of file watcher `selfChanges` map (prevents memory leak)
53-
- **Perf:** added missing `idx_conflicts_tracked_file` database index
33+
- Fix: `.gitignore` managed block syncs with pattern settings changes
34+
- Fix: false conflicts from uncommitted store git changes
35+
- Fix: ignore pattern changes take effect immediately and restart watchers
36+
- File tree: search, right-click "Ignore", show largest files toggle
37+
- Per-service ignore pattern overrides
38+
- Security: path traversal protection, command injection fix, CORS/host lockdown, symlink validation, WebSocket payload limit, sync_log auto-cleanup
39+
- Perf: N+1 query fix, non-overlapping sync cycles, picomatch caching, DB index
5440

5541
## v1.4.0
5642

57-
- **New:** AI Service Config sync: sync local AI service settings (e.g., `~/.claude/` for Claude Code)
58-
- Merged repo and service detail pages into a shared component (reduced code duplication)
59-
- **New:** Showing size information:
60-
- Show total store size per repo/service on dashboard cards and detail pages
61-
- Per-file size display in repo/service detail file tree
62-
- Aggregated folder sizes in file tree (computed client-side from per-file data)
63-
- Size warning indicators: yellow (20-50 MB), red (50-100 MB)
64-
- Sync blocked automatically when store size exceeds 100 MB with clear banner
65-
- Customizable size thresholds (warning, danger, block) in Settings > General
43+
- AI Service Config sync (e.g., `~/.claude/` for Claude Code)
44+
- Store size display on dashboard and detail pages with warning indicators
45+
- Sync blocked when store size exceeds threshold (configurable in Settings)
6646

6747
## v1.3.1
6848

69-
- Local patterns now always on top, global below (with divider) in repo settings
49+
- Local patterns always shown above global patterns in repo settings
7050

7151
## v1.3.0
7252

73-
- **New:** Per-repository settings with local overrides for general settings, AI file patterns, and ignore patterns
74-
- **New:** Update notification from GitHub Releases
75-
- **New:** Clone files/folders across repos with conflict preview
76-
- **New:** Dashboard filter for repos with unresolved conflicts
53+
- Per-repository settings with local overrides
54+
- Update notification from GitHub Releases
55+
- Clone files/folders across repos with conflict preview
56+
- Dashboard filter for unresolved conflicts
7757

7858
## v1.2.1
7959

80-
- **Fix:** "Clean" in settings now also commits in git
81-
- **Fix:** `__pycache__/` pattern not ignored correctly
82-
- **Fix:** Conflict warnings not refreshing on repo removal
60+
- Fix: "Clean" now commits in git, `__pycache__/` ignore, conflict refresh on repo removal
8361

8462
## v1.2.0
8563

86-
- **New:** Desktop app (Tauri v2) with bundled sidecar server
64+
- Desktop app (Tauri v2) — later removed in v1.5.0
8765
- Rebranded from `local-ai-stuffs` to `ai-sync`
8866

89-
## v1.1.1 & v1.1.2
90-
91-
- UI improvements
92-
93-
## v1.1.0
67+
## v1.1.x
9468

95-
- **New:** Ignore patterns with configurable globs and "Clean" button
96-
- **New:** Favorite repositories
97-
- **New:** Symbolic link support
98-
- **New:** New default patterns: `.agent/**`, `.agents/**`, `.gemini/**`, `.github/skills/**`, `.opencode/**`
99-
- **Fix:** conflict resolver refresh, editor content updates, symlink sync
69+
- Ignore patterns with configurable globs and "Clean" button
70+
- Favorite repositories, symbolic link support
71+
- Default patterns: `.agent/**`, `.agents/**`, `.gemini/**`, `.github/skills/**`, `.opencode/**`
72+
- Bug fixes and UI improvements
10073

10174
## v1.0.0
10275

10376
- Central store as git repo for AI config files
104-
- Bidirectional sync with chokidar watchers
105-
- Git-based 3-way merge and auto-merge
106-
- Web Admin UI (React SPA, CodeMirror 6 editor, conflict resolver)
77+
- Bidirectional sync with git-based 3-way merge
78+
- Web Admin UI with CodeMirror editor and conflict resolver
10779
- Template system, file pattern management, gitignore management
108-
- Remote publishing, WebSocket events, setup wizard, pause/resume
10980
- Supported: Claude Code, Cursor, Gemini, GitHub Copilot, Aider, Windsurf

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": "1.6.0",
4+
"version": "1.7.0",
55
"repository": "https://github.com/anhthiding/ai-sync",
66
"private": true,
77
"license": "MIT",

packages/server/package.json

Lines changed: 2 additions & 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": "1.6.0",
4+
"version": "1.7.0",
55
"repository": "https://github.com/anhthiding/ai-sync",
66
"private": true,
77
"license": "MIT",
@@ -15,6 +15,7 @@
1515
},
1616
"dependencies": {
1717
"@fastify/cors": "^11.0.0",
18+
"@fastify/multipart": "^9.4.0",
1819
"@fastify/static": "^8.1.0",
1920
"@fastify/websocket": "^11.0.0",
2021
"better-sqlite3": "^11.7.0",

packages/server/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Fastify from 'fastify';
22
import fastifyStatic from '@fastify/static';
33
import fastifyCors from '@fastify/cors';
44
import fastifyWebsocket from '@fastify/websocket';
5+
import fastifyMultipart from '@fastify/multipart';
56
import fs from 'node:fs';
67
import { config } from './config.js';
78
import { registerRepoRoutes } from './routes/repos.js';
@@ -25,6 +26,7 @@ export async function buildApp(state: AppState) {
2526
origin: [/^https?:\/\/localhost(:\d+)?$/, /^https?:\/\/127\.0\.0\.1(:\d+)?$/],
2627
});
2728
await app.register(fastifyWebsocket, { options: { maxPayload: 1048576 } });
29+
await app.register(fastifyMultipart, { limits: { fileSize: 2 * 1024 * 1024 } });
2830

2931
// All routes are always registered — they check state.db/state.syncEngine internally
3032
registerSetupRoutes(app, state);

packages/server/src/db/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Database from 'better-sqlite3';
22
import fs from 'node:fs';
33
import path from 'node:path';
44
import { initSchema } from './schema.js';
5+
import { registerCustomDefinition } from '../services/service-definitions.js';
56

67
let db: Database.Database | null = null;
78

@@ -16,6 +17,29 @@ export function initDb(dbPath: string): Database.Database {
1617
return db;
1718
}
1819

20+
/**
21+
* Load custom service definitions from DB into the runtime registry.
22+
* Custom services have service_type starting with "custom-".
23+
*/
24+
export function loadCustomServiceDefinitions(database: Database.Database): void {
25+
const rows = database
26+
.prepare(
27+
"SELECT service_type, name, local_path FROM service_configs WHERE service_type LIKE 'custom-%'",
28+
)
29+
.all() as { service_type: string; name: string; local_path: string }[];
30+
31+
for (const row of rows) {
32+
// Patterns are stored in service_settings, not in the definition.
33+
// registerCustomDefinition sets patterns to [] to avoid duplication.
34+
registerCustomDefinition({
35+
serviceType: row.service_type,
36+
name: row.name,
37+
defaultPath: row.local_path,
38+
patterns: [],
39+
});
40+
}
41+
}
42+
1943
export function getDb(): Database.Database {
2044
if (!db) {
2145
throw new Error('Database not initialized. Call initDb() first.');

packages/server/src/db/schema.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,13 @@ function runMigrations(db: Database.Database): void {
240240
CREATE INDEX IF NOT EXISTS idx_sync_log_created ON sync_log(created_at);
241241
`,
242242
},
243+
{
244+
version: 10,
245+
sql: `
246+
ALTER TABLE service_configs ADD COLUMN description TEXT NOT NULL DEFAULT '';
247+
ALTER TABLE service_configs ADD COLUMN icon_path TEXT DEFAULT NULL;
248+
`,
249+
},
243250
];
244251

245252
for (const m of migrations) {

packages/server/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { buildApp } from './app.js';
2-
import { initDb } from './db/index.js';
2+
import { initDb, loadCustomServiceDefinitions } from './db/index.js';
33
import { config, isConfigured, ensureMachineId } from './config.js';
44
import { initStoreRepo, commitStoreChanges } from './services/store-git.js';
55
import { SyncEngine } from './services/sync-engine.js';
@@ -15,6 +15,7 @@ async function main() {
1515
ensureMachineId();
1616

1717
state.db = initDb(config.dbPath);
18+
loadCustomServiceDefinitions(state.db);
1819
console.log(`Database initialized at ${config.dbPath}`);
1920

2021
await initStoreRepo();

0 commit comments

Comments
 (0)