Skip to content

Commit f9761df

Browse files
committed
feat: multi-machines supported
chore chore: add author info to package.json files test: add test cases for muti-machines chore
1 parent 38b2c73 commit f9761df

27 files changed

+2075
-25
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Centralized management tool for AI configuration files across Git repositories a
1414
- A **central store** (a separate git repo you choose) holds all AI config files, organized by repository and service
1515
- A **sync engine** watches for changes on both sides and syncs automatically using **git 3-way merge** — non-conflicting changes are auto-merged
1616
- **AI service configs** (e.g., Claude Code's `~/.claude/`) can be synced with predefined file patterns — no manual path browsing needed
17+
- **Multi-machine support** — each machine gets a unique identity; a git-tracked `machines.json` maps repo paths per machine, so the same store works across machines with different directory layouts. Repos are auto-linked on startup
1718
- A **web UI** lets you manage repos, services, edit files, and resolve conflicts
1819
- AI files are automatically **git-ignored** and **removed from git tracking** in target repos
1920
- **App code and user data are fully separated** — update the tool without affecting your data
@@ -50,6 +51,7 @@ Open http://localhost:2703
5051

5152
```
5253
<your-data-dir>/ # Git repo (you chose this path)
54+
├── machines.json # Machine-to-path mappings (git-tracked)
5355
├── repos/
5456
│ ├── _default/ # Template for new repos
5557
│ ├── my-project/ # AI files for my-project
@@ -74,7 +76,9 @@ cd ai-sync
7476
pnpm install && pnpm build && pnpm start
7577
```
7678

77-
On the setup screen, point to your existing data directory (clone your store repo first if needed). Attach your locally-cloned repos via the UI — files sync automatically.
79+
On the setup screen, point to your existing data directory (clone your store repo first if needed).
80+
81+
The app will automatically assign a machine identity and **auto-link** any repos that already have a path mapping for this machine in `machines.json`. Repos that can't be auto-linked appear as **Unlinked Repositories** on the dashboard — click **Link** to map them to local paths, **Auto-link All** to link everything at once, or the **trash icon** to remove repos you no longer need.
7882

7983
## Development
8084

docs/changelog.md

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

3+
## v1.6.0 - Multi-machine support
4+
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
11+
312
## v1.5.1
413

514
- **New:** Right-click "Delete" on file tree — deletes files/folders from both store and target repo, with empty parent directory cleanup

docs/how-to.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,12 @@ The **Templates** page lets you define default AI config files for new repositor
118118

119119
## Configuring Settings
120120

121-
The **Settings** page has three tabs:
121+
The **Settings** page has four tabs:
122122

123123
- **General** — Sync interval, watch debounce, auto sync, and auto-commit options
124124
- **AI File Patterns** — Glob patterns that detect AI config files (add, remove, or toggle)
125125
- **Ignore Patterns** — Glob patterns to exclude files from sync (e.g., `.DS_Store`, `node_modules/**`). Use the **Clean** button to remove already-tracked files that match ignore patterns
126+
- **Machine** — View/edit machine name, copy machine ID, and see all known machines that share this store
126127

127128
### Per-Repository Settings
128129

@@ -137,7 +138,38 @@ If your data directory is connected to a remote git repository, use the **Push c
137138
1. Clone this tool repository then run `pnpm install && pnpm build && pnpm start`, the app will start at [http://localhost:2703](http://localhost:2703) in your browser.
138139
2. Clone your store repository (data directory)
139140
3. On the setup screen, point to the cloned store directory
140-
4. Add your locally-cloned repos via the UI — files sync automatically
141+
4. The app will automatically:
142+
- Assign a unique machine ID and name (based on hostname)
143+
- Register this machine in `machines.json`
144+
- Auto-link any repos that have known paths for this machine
145+
5. Repos that couldn't be auto-linked will appear as **Unlinked Repositories** on the dashboard — click **Link** to map them to local paths, or **Auto-link All** if paths are already mapped from another machine
146+
6. To remove an unlinked repo you no longer need, click the **trash icon** on its card to delete it from the store
147+
148+
## Multi-Machine Workflow
149+
150+
AI Sync supports using the same store across multiple machines, even when repos live at different paths on each machine.
151+
152+
### How It Works
153+
154+
- Each machine gets a **unique ID** (UUID) and a **name** (defaults to your hostname), stored locally in `~/.ai-sync/config.json`
155+
- A **`machines.json`** file in the store repo tracks which machine has which repo at which local path
156+
- When you add a repo on Machine A, the path mapping is saved in `machines.json`. When Machine B pulls the store, it can use that mapping to auto-link or manually link the repo
157+
158+
### Managing Unlinked Repos
159+
160+
When the store contains repos that aren't linked on the current machine, they appear in the **Unlinked Repositories** section on the dashboard:
161+
162+
- **Auto-link All** — Automatically link all repos that have a valid path mapping for this machine
163+
- **Link** — Manually specify the local path for a specific repo
164+
- **Delete** — Remove the repo from the store entirely (trash icon on each card)
165+
166+
### Machine Settings
167+
168+
Go to **Settings → Machine** to:
169+
170+
- **Edit machine name** — Change the display name for this machine
171+
- **View machine ID** — Copy the unique identifier for debugging
172+
- **See all machines** — List every machine that has ever connected to this store, with last-seen dates
141173

142174
## Keyboard Shortcuts
143175

docs/intro.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ AI Sync provides a single-source-of-truth approach:
2727
1. **Central Store** — All AI config files are stored in a user-chosen directory (a separate git repo)
2828
2. **Automatic Sync** — A background service watches for file changes and syncs bidirectionally between the store and target repositories
2929
3. **AI Service Configs** — Sync local AI service settings (e.g., `~/.claude/` for Claude Code) with predefined file patterns — no manual path browsing needed
30-
4. **Web Admin UI** — A local web interface for managing repositories, editing files, resolving conflicts, and monitoring sync status
31-
5. **Portable** — Clone this tool on any machine, point it to your store directory, attach your repositories, and all AI configs sync automatically
32-
6. **Separated Data** — App code and user data live in different directories — update the tool without affecting your data
33-
7. **Symbolic Link Support** — Symlinks are properly detected, tracked, and synced between store and target repositories
34-
8. **Ignore Patterns** — Configurable glob patterns to exclude unwanted files (e.g., `.DS_Store`, `node_modules/**`) from sync
30+
4. **Multi-Machine Support** — Each machine gets a unique identity. A git-tracked `machines.json` maps repo paths per machine, so the same store works across machines with different directory structures. Repos are auto-linked on startup when valid path mappings exist
31+
5. **Web Admin UI** — A local web interface for managing repositories, editing files, resolving conflicts, and monitoring sync status
32+
6. **Portable** — Clone this tool on any machine, point it to your store directory, and repos with known paths are auto-linked. Unlinked repos can be manually linked via the dashboard
33+
7. **Separated Data** — App code and user data live in different directories — update the tool without affecting your data
34+
8. **Symbolic Link Support** — Symlinks are properly detected, tracked, and synced between store and target repositories
35+
9. **Ignore Patterns** — Configurable glob patterns to exclude unwanted files (e.g., `.DS_Store`, `node_modules/**`) from sync
3536

3637
## How Sync Works
3738

@@ -57,6 +58,18 @@ When both sides changed:
5758

5859
A checksum fast-path ensures git is only invoked when files actually differ, keeping the common case fast.
5960

61+
## Multi-Machine Support
62+
63+
When multiple machines share the same store repository (data directory) via git, each machine may have different absolute paths for the same repository (e.g., `/Users/thi/git/project` on a Mac vs `/home/thi/code/project` on Linux).
64+
65+
AI Sync handles this with:
66+
67+
- **Machine Identity** — Each machine gets a unique UUID and a human-readable name (defaults to hostname), stored in the local config (`~/.ai-sync/config.json`)
68+
- **`machines.json`** — A git-tracked file in the store repo that maps each repo/service to each machine's local path. All machines see each other's mappings through git sync
69+
- **Auto-linking** — On startup, if the store contains repos with known paths for the current machine, they are automatically registered in the local database and start syncing
70+
- **Unlinked repos** — The dashboard shows store repos that exist but aren't linked on the current machine, with options to link manually, auto-link, or delete from the store
71+
- **Machine settings** — View and edit the machine name, see the machine ID, and list all known machines in the Settings page
72+
6073
## Architecture Overview
6174

6275
```
@@ -75,7 +88,7 @@ A checksum fast-path ensures git is only invoked when files actually differ, kee
7588
│ └──────────────┘ └──────────────┘ └───────────────┘ │
7689
└─────────────────────────────────────────────────────────┘
7790
│ │ │
78-
┌───────┴───────┐ ┌────────┴───────┐ ┌────┴───────────┐
91+
┌───────┴───────┐ ┌───-─────┴───────┐ ┌──-──┴───────────┐
7992
│ Store │ │ Target Repos │ │ AI Services │
8093
│ (user chosen) │ │ /path/to/repo-1 │ │ ~/.claude/ │
8194
└───────────────┘ │ /path/to/repo-2 │ │ (Claude Code) │

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
"name": "ai-sync",
3-
"version": "1.5.1",
3+
"author": "Anh-Thi Dinh",
4+
"version": "1.6.0",
5+
"repository": "https://github.com/anhthiding/ai-sync",
46
"private": true,
57
"license": "MIT",
68
"description": "Centralized management tool for AI configuration files across Git repositories",

packages/server/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
"name": "@ai-sync/server",
3-
"version": "1.5.1",
3+
"author": "Anh-Thi Dinh",
4+
"version": "1.6.0",
5+
"repository": "https://github.com/anhthiding/ai-sync",
46
"private": true,
57
"license": "MIT",
68
"type": "module",

packages/server/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { registerCloneRoutes } from './routes/clone.js';
1414
import { registerServiceRoutes } from './routes/services.js';
1515
import { registerSetupRoutes } from './routes/setup.js';
1616
import { registerVersionRoutes } from './routes/version.js';
17+
import { registerMachineRoutes } from './routes/machines.js';
1718
import { registerWsHandlers } from './ws/handlers.js';
1819
import type { AppState } from './app-state.js';
1920

@@ -35,6 +36,7 @@ export async function buildApp(state: AppState) {
3536
registerTemplateRoutes(app, state);
3637
registerCloneRoutes(app, state);
3738
registerServiceRoutes(app, state);
39+
registerMachineRoutes(app, state);
3840
registerVersionRoutes(app);
3941
registerWsHandlers(app, state);
4042

packages/server/src/config.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from 'node:path';
22
import fs from 'node:fs';
33
import { fileURLToPath } from 'node:url';
44
import os from 'node:os';
5+
import { v4 as uuid } from 'uuid';
56

67
const __dirname = path.dirname(fileURLToPath(import.meta.url));
78
const projectRoot = path.resolve(__dirname, '..', '..', '..');
@@ -17,6 +18,8 @@ const LEGACY_CONFIG_FILE = path.join(LEGACY_CONFIG_DIR, 'config.json');
1718

1819
interface AppConfig {
1920
dataDir: string;
21+
machineId?: string;
22+
machineName?: string;
2023
}
2124

2225
function readAppConfig(): AppConfig | null {
@@ -29,7 +32,11 @@ function readAppConfig(): AppConfig | null {
2932
const raw = fs.readFileSync(configFile, 'utf-8');
3033
const parsed = JSON.parse(raw);
3134
if (parsed.dataDir && typeof parsed.dataDir === 'string') {
32-
return { dataDir: parsed.dataDir };
35+
return {
36+
dataDir: parsed.dataDir,
37+
machineId: parsed.machineId,
38+
machineName: parsed.machineName,
39+
};
3340
}
3441
} catch {
3542
// Config file doesn't exist, try next
@@ -38,6 +45,11 @@ function readAppConfig(): AppConfig | null {
3845
return null;
3946
}
4047

48+
function writeAppConfig(appCfg: AppConfig): void {
49+
fs.mkdirSync(APP_CONFIG_DIR, { recursive: true });
50+
fs.writeFileSync(APP_CONFIG_FILE, JSON.stringify(appCfg, null, 2), 'utf-8');
51+
}
52+
4153
function buildDataPaths(dataDir: string) {
4254
// Use legacy DB name if it exists, otherwise use new name
4355
const legacyDbPath = path.join(dataDir, '.db', 'local-ai-stuffs.db');
@@ -68,6 +80,8 @@ export const config = {
6880
storeReposPath: dataPaths?.storeReposPath || '',
6981
storeServicesPath: dataPaths?.storeServicesPath || '',
7082
dbPath: dataPaths?.dbPath || '',
83+
machineId: appConfig?.machineId || '',
84+
machineName: appConfig?.machineName || '',
7185
};
7286

7387
export function isConfigured(): boolean {
@@ -79,8 +93,13 @@ export function getDataDir(): string | null {
7993
}
8094

8195
export function configure(dataDir: string): void {
82-
fs.mkdirSync(APP_CONFIG_DIR, { recursive: true });
83-
fs.writeFileSync(APP_CONFIG_FILE, JSON.stringify({ dataDir }, null, 2), 'utf-8');
96+
const existing = readAppConfig();
97+
const appCfg: AppConfig = {
98+
dataDir,
99+
machineId: existing?.machineId,
100+
machineName: existing?.machineName,
101+
};
102+
writeAppConfig(appCfg);
84103

85104
const paths = buildDataPaths(dataDir);
86105
config.dataDir = dataDir;
@@ -101,4 +120,34 @@ export function resetConfig(): void {
101120
config.storeReposPath = '';
102121
config.storeServicesPath = '';
103122
config.dbPath = '';
123+
config.machineId = '';
124+
config.machineName = '';
125+
}
126+
127+
export function ensureMachineId(): void {
128+
const appCfg = readAppConfig();
129+
if (!appCfg) return;
130+
131+
let changed = false;
132+
if (!appCfg.machineId) {
133+
appCfg.machineId = uuid();
134+
changed = true;
135+
}
136+
if (!appCfg.machineName) {
137+
appCfg.machineName = os.hostname();
138+
changed = true;
139+
}
140+
if (changed) {
141+
writeAppConfig(appCfg);
142+
}
143+
config.machineId = appCfg.machineId;
144+
config.machineName = appCfg.machineName!;
145+
}
146+
147+
export function updateMachineName(name: string): void {
148+
const appCfg = readAppConfig();
149+
if (!appCfg) return;
150+
appCfg.machineName = name;
151+
writeAppConfig(appCfg);
152+
config.machineName = name;
104153
}

packages/server/src/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { buildApp } from './app.js';
22
import { initDb } from './db/index.js';
3-
import { config, isConfigured } from './config.js';
4-
import { initStoreRepo } from './services/store-git.js';
3+
import { config, isConfigured, ensureMachineId } from './config.js';
4+
import { initStoreRepo, commitStoreChanges } from './services/store-git.js';
55
import { SyncEngine } from './services/sync-engine.js';
6+
import { registerCurrentMachine, seedMachinesFile, autoLinkRepos } from './services/machines.js';
67
import type { AppState } from './app-state.js';
78

89
async function main() {
@@ -11,12 +12,24 @@ async function main() {
1112
const state: AppState = { db: null, syncEngine: null };
1213

1314
if (isConfigured()) {
15+
ensureMachineId();
16+
1417
state.db = initDb(config.dbPath);
1518
console.log(`Database initialized at ${config.dbPath}`);
1619

1720
await initStoreRepo();
1821
console.log(`Store initialized at ${config.storePath}`);
1922

23+
// Register this machine and seed/auto-link repos
24+
registerCurrentMachine();
25+
seedMachinesFile(state.db);
26+
const linkResults = await autoLinkRepos(state.db);
27+
const linked = linkResults.filter((r) => r.status === 'linked');
28+
if (linked.length > 0) {
29+
console.log(`Auto-linked ${linked.length} repo(s) from machines.json`);
30+
}
31+
await commitStoreChanges(`Machine ${config.machineName} startup`);
32+
2033
state.syncEngine = new SyncEngine(state.db);
2134
} else {
2235
console.log('Not configured yet — starting in setup mode');

0 commit comments

Comments
 (0)