@geminilight/mindos 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -11
- package/README_zh.md +3 -6
- package/app/app/api/sync/route.ts +124 -0
- package/app/components/SettingsModal.tsx +3 -0
- package/app/components/settings/SyncTab.tsx +219 -0
- package/app/components/settings/types.ts +1 -1
- package/app/lib/i18n.ts +2 -2
- package/app/lib/settings.ts +1 -1
- package/assets/demo-flow-zh.html +30 -30
- package/assets/images/demo-flow-dark.png +0 -0
- package/assets/images/demo-flow-light.png +0 -0
- package/assets/images/demo-flow-zh-dark.png +0 -0
- package/assets/images/demo-flow-zh-light.png +0 -0
- package/bin/cli.js +234 -692
- package/bin/lib/build.js +59 -0
- package/bin/lib/colors.js +7 -0
- package/bin/lib/config.js +58 -0
- package/bin/lib/constants.js +13 -0
- package/bin/lib/gateway.js +244 -0
- package/bin/lib/mcp-install.js +156 -0
- package/bin/lib/mcp-spawn.js +36 -0
- package/bin/lib/pid.js +15 -0
- package/bin/lib/port.js +19 -0
- package/bin/lib/startup.js +51 -0
- package/bin/lib/stop.js +27 -0
- package/bin/lib/sync.js +367 -0
- package/bin/lib/utils.js +16 -0
- package/package.json +6 -2
- package/scripts/release.sh +56 -0
- package/scripts/setup.js +23 -5
package/README.md
CHANGED
|
@@ -82,6 +82,7 @@ Static documents are hard to synchronize and weak as execution systems in real h
|
|
|
82
82
|
- **Reference Sync**: keep cross-file status and context aligned via links/backlinks.
|
|
83
83
|
- **Knowledge Graph**: visualize relationships and dependencies across notes.
|
|
84
84
|
- **Git Time Machine**: track every edit, audit history, and roll back safely.
|
|
85
|
+
- **Cross-Device Sync**: auto-commit, push, and pull via Git — edits on one device appear on all others within minutes.
|
|
85
86
|
|
|
86
87
|
<details>
|
|
87
88
|
<summary><strong>Coming Soon</strong></summary>
|
|
@@ -125,11 +126,9 @@ npm link # registers the `mindos` command globally
|
|
|
125
126
|
### 2. Interactive Setup
|
|
126
127
|
|
|
127
128
|
```bash
|
|
128
|
-
mindos onboard
|
|
129
|
+
mindos onboard
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
> `--install-daemon`: after setup, automatically installs and starts MindOS as a background OS service (survives terminal close, auto-restarts on crash).
|
|
132
|
-
|
|
133
132
|
The setup wizard will guide you through:
|
|
134
133
|
1. Knowledge base path → default `~/.mindos/my-mind`
|
|
135
134
|
2. Choose template (en / zh / empty / custom)
|
|
@@ -137,6 +136,7 @@ The setup wizard will guide you through:
|
|
|
137
136
|
4. Auth token (auto-generated or passphrase-seeded)
|
|
138
137
|
5. Web UI password (optional)
|
|
139
138
|
6. AI Provider (Anthropic / OpenAI) + API Key — or **skip** to configure later via `mindos config set`
|
|
139
|
+
7. Start mode — **Background service** (recommended, auto-starts on boot) or Foreground
|
|
140
140
|
|
|
141
141
|
Config is saved to `~/.mindos/config.json` automatically.
|
|
142
142
|
|
|
@@ -158,12 +158,21 @@ Or skip the wizard and edit `~/.mindos/config.json` manually (see Config Referen
|
|
|
158
158
|
"mcpPort": 8787,
|
|
159
159
|
"authToken": "",
|
|
160
160
|
"webPassword": "",
|
|
161
|
+
"startMode": "daemon",
|
|
161
162
|
"ai": {
|
|
162
163
|
"provider": "anthropic",
|
|
163
164
|
"providers": {
|
|
164
165
|
"anthropic": { "apiKey": "sk-ant-...", "model": "claude-sonnet-4-6" },
|
|
165
166
|
"openai": { "apiKey": "sk-...", "model": "gpt-5.4", "baseUrl": "" }
|
|
166
167
|
}
|
|
168
|
+
},
|
|
169
|
+
"sync": {
|
|
170
|
+
"enabled": true,
|
|
171
|
+
"provider": "git",
|
|
172
|
+
"remote": "origin",
|
|
173
|
+
"branch": "main",
|
|
174
|
+
"autoCommitInterval": 30,
|
|
175
|
+
"autoPullInterval": 300
|
|
167
176
|
}
|
|
168
177
|
}
|
|
169
178
|
```
|
|
@@ -175,12 +184,19 @@ Or skip the wizard and edit `~/.mindos/config.json` manually (see Config Referen
|
|
|
175
184
|
| `mcpPort` | `8787` | Optional. MCP server port. |
|
|
176
185
|
| `authToken` | — | Optional. Protects App `/api/*` and MCP `/mcp` with bearer token auth. For Agent / MCP clients. Recommended when exposed to a network. |
|
|
177
186
|
| `webPassword` | — | Optional. Protects the web UI with a login page. For browser access. Independent from `authToken`. |
|
|
187
|
+
| `startMode` | `start` | Start mode: `daemon` (background service, auto-starts on boot), `start` (foreground), or `dev`. |
|
|
178
188
|
| `ai.provider` | `anthropic` | Active provider: `anthropic` or `openai`. |
|
|
179
189
|
| `ai.providers.anthropic.apiKey` | — | Anthropic API key. |
|
|
180
190
|
| `ai.providers.anthropic.model` | `claude-sonnet-4-6` | Anthropic model ID. |
|
|
181
191
|
| `ai.providers.openai.apiKey` | — | OpenAI API key. |
|
|
182
192
|
| `ai.providers.openai.model` | `gpt-5.4` | OpenAI model ID. |
|
|
183
193
|
| `ai.providers.openai.baseUrl` | — | Optional. Custom endpoint for proxy or OpenAI-compatible APIs. |
|
|
194
|
+
| `sync.enabled` | `false` | Enable/disable automatic Git sync. |
|
|
195
|
+
| `sync.provider` | `git` | Sync provider (currently only `git`). |
|
|
196
|
+
| `sync.remote` | `origin` | Git remote name. |
|
|
197
|
+
| `sync.branch` | `main` | Git branch to sync. |
|
|
198
|
+
| `sync.autoCommitInterval` | `30` | Seconds after file change to auto-commit+push. |
|
|
199
|
+
| `sync.autoPullInterval` | `300` | Seconds between auto-pull from remote. |
|
|
184
200
|
|
|
185
201
|
Multiple providers can be configured simultaneously — switch between them by changing `ai.provider`. Shell env vars (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.) take precedence over config file values.
|
|
186
202
|
|
|
@@ -190,7 +206,13 @@ Multiple providers can be configured simultaneously — switch between them by c
|
|
|
190
206
|
> If you want the MindOS GUI to be reachable from other devices, make sure the port is open in firewall/security-group settings and bound to an accessible host/network interface.
|
|
191
207
|
|
|
192
208
|
> [!TIP]
|
|
193
|
-
>
|
|
209
|
+
> If you chose "Background service" during onboard, MindOS is installed as a background OS service and starts automatically — no need to run `mindos start` manually. Run `mindos update` to upgrade to the latest version.
|
|
210
|
+
|
|
211
|
+
Open the Web UI in your browser:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
mindos open
|
|
215
|
+
```
|
|
194
216
|
|
|
195
217
|
### 3. Inject Your Personal Mind with MindOS Agent
|
|
196
218
|
|
|
@@ -225,7 +247,7 @@ Use `stdio` transport — no server process needed, most reliable:
|
|
|
225
247
|
mindos mcp install
|
|
226
248
|
|
|
227
249
|
# One-shot, global scope (shared across all projects)
|
|
228
|
-
mindos mcp install
|
|
250
|
+
mindos mcp install -g -y
|
|
229
251
|
```
|
|
230
252
|
|
|
231
253
|
**Remote (Agent on a different machine)**
|
|
@@ -233,7 +255,7 @@ mindos mcp install claude-code -g -y
|
|
|
233
255
|
Use `http` transport — MindOS must be running (`mindos start`) on the remote machine:
|
|
234
256
|
|
|
235
257
|
```bash
|
|
236
|
-
mindos mcp install
|
|
258
|
+
mindos mcp install--transport http --url http://<server-ip>:8787/mcp --token your-token -g
|
|
237
259
|
```
|
|
238
260
|
|
|
239
261
|
> [!NOTE]
|
|
@@ -386,12 +408,13 @@ MindOS/
|
|
|
386
408
|
├── mcp/ # MCP Server — HTTP adapter that maps tools to App API
|
|
387
409
|
├── skills/ # MindOS Skills (`mindos`, `mindos-zh`) — Workflow guides for Agents
|
|
388
410
|
├── templates/ # Preset templates (`en/`, `zh/`, `empty/`) — copied to knowledge base on onboard
|
|
389
|
-
├── bin/ # CLI entry point (`mindos onboard`, `mindos start`, `mindos
|
|
411
|
+
├── bin/ # CLI entry point (`mindos onboard`, `mindos start`, `mindos open`, `mindos sync`, `mindos token`)
|
|
390
412
|
├── scripts/ # Setup wizard and helper scripts
|
|
391
413
|
└── README.md
|
|
392
414
|
|
|
393
415
|
~/.mindos/ # User data directory (outside project, never committed)
|
|
394
|
-
├── config.json # All configuration (AI keys, port, auth token,
|
|
416
|
+
├── config.json # All configuration (AI keys, port, auth token, sync settings)
|
|
417
|
+
├── sync-state.json # Sync state (last sync time, conflicts)
|
|
395
418
|
└── my-mind/ # Your private knowledge base (default path, customizable on onboard)
|
|
396
419
|
```
|
|
397
420
|
|
|
@@ -401,17 +424,24 @@ MindOS/
|
|
|
401
424
|
|
|
402
425
|
| Command | Description |
|
|
403
426
|
| :--- | :--- |
|
|
404
|
-
| `mindos onboard` | Interactive setup (config, template
|
|
405
|
-
| `mindos onboard --install-daemon` | Setup + install & start as background OS service |
|
|
427
|
+
| `mindos onboard` | Interactive setup (config, template, start mode) |
|
|
406
428
|
| `mindos start` | Start app + MCP server (foreground, production mode) |
|
|
407
429
|
| `mindos start --daemon` | Install + start as a background OS service (survives terminal close, auto-restarts on crash) |
|
|
408
430
|
| `mindos dev` | Start app + MCP server (dev mode, hot reload) |
|
|
409
431
|
| `mindos dev --turbopack` | Dev mode with Turbopack (faster HMR) |
|
|
432
|
+
| `mindos open` | Open the Web UI in the default browser |
|
|
410
433
|
| `mindos stop` | Stop running MindOS processes |
|
|
411
434
|
| `mindos restart` | Stop then start again |
|
|
412
435
|
| `mindos build` | Manually build for production |
|
|
413
436
|
| `mindos mcp` | Start MCP server only |
|
|
414
|
-
| `mindos token` | Show
|
|
437
|
+
| `mindos token` | Show auth token and per-agent MCP config snippets |
|
|
438
|
+
| `mindos sync` | Show sync status (alias for `sync status`) |
|
|
439
|
+
| `mindos sync init` | Interactive setup for Git remote sync |
|
|
440
|
+
| `mindos sync status` | Show sync status: last sync, unpushed commits, conflicts |
|
|
441
|
+
| `mindos sync now` | Manually trigger a full sync (commit + push + pull) |
|
|
442
|
+
| `mindos sync on` | Enable automatic sync |
|
|
443
|
+
| `mindos sync off` | Disable automatic sync |
|
|
444
|
+
| `mindos sync conflicts` | List unresolved conflict files |
|
|
415
445
|
| `mindos gateway install` | Install background service (systemd on Linux, LaunchAgent on macOS) |
|
|
416
446
|
| `mindos gateway uninstall` | Remove background service |
|
|
417
447
|
| `mindos gateway start` | Start the background service |
|
package/README_zh.md
CHANGED
|
@@ -222,13 +222,10 @@ mindos mcp install
|
|
|
222
222
|
|
|
223
223
|
```bash
|
|
224
224
|
# 交互式
|
|
225
|
-
mindos mcp install
|
|
226
|
-
|
|
227
|
-
# 一键安装,无需交互(stdio + 项目级)
|
|
228
|
-
mindos mcp install claude-code -y
|
|
225
|
+
mindos mcp install
|
|
229
226
|
|
|
230
227
|
# 一键安装,全局(所有项目共享)
|
|
231
|
-
mindos mcp install
|
|
228
|
+
mindos mcp install -g -y
|
|
232
229
|
```
|
|
233
230
|
|
|
234
231
|
**远程访问(Agent 在另一台机器)**
|
|
@@ -236,7 +233,7 @@ mindos mcp install claude-code -g -y
|
|
|
236
233
|
使用 `http` transport — 远程机器上需先运行 `mindos start`:
|
|
237
234
|
|
|
238
235
|
```bash
|
|
239
|
-
mindos mcp install
|
|
236
|
+
mindos mcp install --transport http --url http://<服务器IP>:8787/mcp --token your-token -g
|
|
240
237
|
```
|
|
241
238
|
|
|
242
239
|
> [!NOTE]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export const dynamic = 'force-dynamic';
|
|
2
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
|
|
8
|
+
const MINDOS_DIR = join(homedir(), '.mindos');
|
|
9
|
+
const CONFIG_PATH = join(MINDOS_DIR, 'config.json');
|
|
10
|
+
const SYNC_STATE_PATH = join(MINDOS_DIR, 'sync-state.json');
|
|
11
|
+
|
|
12
|
+
function loadConfig() {
|
|
13
|
+
try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch { return {}; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function saveConfig(config: Record<string, unknown>) {
|
|
17
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadSyncState() {
|
|
21
|
+
try { return JSON.parse(readFileSync(SYNC_STATE_PATH, 'utf-8')); } catch { return {}; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getRemoteUrl(cwd: string) {
|
|
25
|
+
try { return execSync('git remote get-url origin', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim(); } catch { return null; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getBranch(cwd: string) {
|
|
29
|
+
try { return execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim(); } catch { return 'main'; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getUnpushedCount(cwd: string) {
|
|
33
|
+
try { return execSync('git rev-list --count @{u}..HEAD', { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim(); } catch { return '?'; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isGitRepo(dir: string) {
|
|
37
|
+
return existsSync(join(dir, '.git'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function GET() {
|
|
41
|
+
const config = loadConfig();
|
|
42
|
+
const syncConfig = config.sync || {};
|
|
43
|
+
const state = loadSyncState();
|
|
44
|
+
const mindRoot = config.mindRoot;
|
|
45
|
+
|
|
46
|
+
if (!syncConfig.enabled) {
|
|
47
|
+
return NextResponse.json({ enabled: false });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const remote = mindRoot && isGitRepo(mindRoot) ? getRemoteUrl(mindRoot) : null;
|
|
51
|
+
const branch = mindRoot && isGitRepo(mindRoot) ? getBranch(mindRoot) : null;
|
|
52
|
+
const unpushed = mindRoot && isGitRepo(mindRoot) ? getUnpushedCount(mindRoot) : '?';
|
|
53
|
+
|
|
54
|
+
return NextResponse.json({
|
|
55
|
+
enabled: true,
|
|
56
|
+
provider: syncConfig.provider || 'git',
|
|
57
|
+
remote: remote || '(not configured)',
|
|
58
|
+
branch: branch || 'main',
|
|
59
|
+
lastSync: state.lastSync || null,
|
|
60
|
+
lastPull: state.lastPull || null,
|
|
61
|
+
unpushed,
|
|
62
|
+
conflicts: state.conflicts || [],
|
|
63
|
+
lastError: state.lastError || null,
|
|
64
|
+
autoCommitInterval: syncConfig.autoCommitInterval || 30,
|
|
65
|
+
autoPullInterval: syncConfig.autoPullInterval || 300,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function POST(req: NextRequest) {
|
|
70
|
+
try {
|
|
71
|
+
const body = await req.json() as { action: string };
|
|
72
|
+
const config = loadConfig();
|
|
73
|
+
const mindRoot = config.mindRoot;
|
|
74
|
+
|
|
75
|
+
if (!mindRoot) {
|
|
76
|
+
return NextResponse.json({ error: 'No mindRoot configured' }, { status: 400 });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
switch (body.action) {
|
|
80
|
+
case 'now': {
|
|
81
|
+
if (!isGitRepo(mindRoot)) {
|
|
82
|
+
return NextResponse.json({ error: 'Not a git repository' }, { status: 400 });
|
|
83
|
+
}
|
|
84
|
+
// Pull
|
|
85
|
+
try { execSync('git pull --rebase --autostash', { cwd: mindRoot, stdio: 'pipe' }); } catch {
|
|
86
|
+
try { execSync('git rebase --abort', { cwd: mindRoot, stdio: 'pipe' }); } catch {}
|
|
87
|
+
try { execSync('git pull --no-rebase', { cwd: mindRoot, stdio: 'pipe' }); } catch {}
|
|
88
|
+
}
|
|
89
|
+
// Commit + push
|
|
90
|
+
execSync('git add -A', { cwd: mindRoot, stdio: 'pipe' });
|
|
91
|
+
const status = execSync('git status --porcelain', { cwd: mindRoot, encoding: 'utf-8' }).trim();
|
|
92
|
+
if (status) {
|
|
93
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
94
|
+
execSync(`git commit -m "auto-sync: ${timestamp}"`, { cwd: mindRoot, stdio: 'pipe' });
|
|
95
|
+
execSync('git push', { cwd: mindRoot, stdio: 'pipe' });
|
|
96
|
+
}
|
|
97
|
+
const state = loadSyncState();
|
|
98
|
+
state.lastSync = new Date().toISOString();
|
|
99
|
+
writeFileSync(SYNC_STATE_PATH, JSON.stringify(state, null, 2) + '\n');
|
|
100
|
+
return NextResponse.json({ ok: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
case 'on': {
|
|
104
|
+
if (!config.sync) config.sync = {};
|
|
105
|
+
config.sync.enabled = true;
|
|
106
|
+
saveConfig(config);
|
|
107
|
+
return NextResponse.json({ ok: true, enabled: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'off': {
|
|
111
|
+
if (!config.sync) config.sync = {};
|
|
112
|
+
config.sync.enabled = false;
|
|
113
|
+
saveConfig(config);
|
|
114
|
+
return NextResponse.json({ ok: true, enabled: false });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
default:
|
|
118
|
+
return NextResponse.json({ error: `Unknown action: ${body.action}` }, { status: 400 });
|
|
119
|
+
}
|
|
120
|
+
} catch (err: unknown) {
|
|
121
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
122
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -13,6 +13,7 @@ import { AppearanceTab } from './settings/AppearanceTab';
|
|
|
13
13
|
import { KnowledgeTab } from './settings/KnowledgeTab';
|
|
14
14
|
import { PluginsTab } from './settings/PluginsTab';
|
|
15
15
|
import { ShortcutsTab } from './settings/ShortcutsTab';
|
|
16
|
+
import { SyncTab } from './settings/SyncTab';
|
|
16
17
|
|
|
17
18
|
interface SettingsModalProps {
|
|
18
19
|
open: boolean;
|
|
@@ -131,6 +132,7 @@ export default function SettingsModal({ open, onClose }: SettingsModalProps) {
|
|
|
131
132
|
{ id: 'ai', label: t.settings.tabs.ai },
|
|
132
133
|
{ id: 'appearance', label: t.settings.tabs.appearance },
|
|
133
134
|
{ id: 'knowledge', label: t.settings.tabs.knowledge },
|
|
135
|
+
{ id: 'sync', label: t.settings.tabs.sync ?? 'Sync' },
|
|
134
136
|
{ id: 'plugins', label: t.settings.tabs.plugins },
|
|
135
137
|
{ id: 'shortcuts', label: t.settings.tabs.shortcuts },
|
|
136
138
|
];
|
|
@@ -193,6 +195,7 @@ export default function SettingsModal({ open, onClose }: SettingsModalProps) {
|
|
|
193
195
|
{tab === 'knowledge' && data && <KnowledgeTab data={data} setData={setData} t={t} />}
|
|
194
196
|
{tab === 'plugins' && <PluginsTab pluginStates={pluginStates} setPluginStates={setPluginStates} t={t} />}
|
|
195
197
|
{tab === 'shortcuts' && <ShortcutsTab t={t} />}
|
|
198
|
+
{tab === 'sync' && <SyncTab t={t} />}
|
|
196
199
|
</>
|
|
197
200
|
)}
|
|
198
201
|
</div>
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { RefreshCw, AlertCircle, CheckCircle2, Loader2 } from 'lucide-react';
|
|
5
|
+
import { SectionLabel } from './Primitives';
|
|
6
|
+
import { apiFetch } from '@/lib/api';
|
|
7
|
+
|
|
8
|
+
interface SyncStatus {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
provider?: string;
|
|
11
|
+
remote?: string;
|
|
12
|
+
branch?: string;
|
|
13
|
+
lastSync?: string | null;
|
|
14
|
+
lastPull?: string | null;
|
|
15
|
+
unpushed?: string;
|
|
16
|
+
conflicts?: Array<{ file: string; time: string }>;
|
|
17
|
+
lastError?: string | null;
|
|
18
|
+
autoCommitInterval?: number;
|
|
19
|
+
autoPullInterval?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SyncTabProps {
|
|
23
|
+
t: any;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function timeAgo(iso: string | null | undefined): string {
|
|
27
|
+
if (!iso) return 'never';
|
|
28
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
29
|
+
if (diff < 60000) return 'just now';
|
|
30
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
31
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
32
|
+
return `${Math.floor(diff / 86400000)}d ago`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SyncTab({ t }: SyncTabProps) {
|
|
36
|
+
const [status, setStatus] = useState<SyncStatus | null>(null);
|
|
37
|
+
const [loading, setLoading] = useState(true);
|
|
38
|
+
const [syncing, setSyncing] = useState(false);
|
|
39
|
+
const [toggling, setToggling] = useState(false);
|
|
40
|
+
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
41
|
+
|
|
42
|
+
const fetchStatus = useCallback(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const data = await apiFetch<SyncStatus>('/api/sync');
|
|
45
|
+
setStatus(data);
|
|
46
|
+
} catch {
|
|
47
|
+
setStatus(null);
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
useEffect(() => { fetchStatus(); }, [fetchStatus]);
|
|
54
|
+
|
|
55
|
+
const handleSyncNow = async () => {
|
|
56
|
+
setSyncing(true);
|
|
57
|
+
setMessage(null);
|
|
58
|
+
try {
|
|
59
|
+
await apiFetch('/api/sync', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ action: 'now' }),
|
|
63
|
+
});
|
|
64
|
+
setMessage({ type: 'success', text: 'Sync complete' });
|
|
65
|
+
await fetchStatus();
|
|
66
|
+
} catch {
|
|
67
|
+
setMessage({ type: 'error', text: 'Sync failed' });
|
|
68
|
+
} finally {
|
|
69
|
+
setSyncing(false);
|
|
70
|
+
setTimeout(() => setMessage(null), 3000);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleToggle = async () => {
|
|
75
|
+
if (!status) return;
|
|
76
|
+
setToggling(true);
|
|
77
|
+
setMessage(null);
|
|
78
|
+
const action = status.enabled ? 'off' : 'on';
|
|
79
|
+
try {
|
|
80
|
+
await apiFetch('/api/sync', {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ action }),
|
|
84
|
+
});
|
|
85
|
+
await fetchStatus();
|
|
86
|
+
setMessage({ type: 'success', text: status.enabled ? 'Auto-sync disabled' : 'Auto-sync enabled' });
|
|
87
|
+
} catch {
|
|
88
|
+
setMessage({ type: 'error', text: 'Failed to toggle sync' });
|
|
89
|
+
} finally {
|
|
90
|
+
setToggling(false);
|
|
91
|
+
setTimeout(() => setMessage(null), 3000);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (loading) {
|
|
96
|
+
return (
|
|
97
|
+
<div className="flex justify-center py-8">
|
|
98
|
+
<Loader2 size={18} className="animate-spin text-muted-foreground" />
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!status || !status.enabled) {
|
|
104
|
+
return (
|
|
105
|
+
<div className="space-y-5">
|
|
106
|
+
<SectionLabel>Sync</SectionLabel>
|
|
107
|
+
<div className="text-sm text-muted-foreground space-y-2">
|
|
108
|
+
<p>Git sync is not configured.</p>
|
|
109
|
+
<p className="text-xs">
|
|
110
|
+
Run <code className="font-mono px-1 py-0.5 bg-muted rounded">mindos sync init</code> in the terminal to set up.
|
|
111
|
+
</p>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const conflicts = status.conflicts || [];
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="space-y-5">
|
|
121
|
+
<SectionLabel>Sync</SectionLabel>
|
|
122
|
+
|
|
123
|
+
{/* Status overview */}
|
|
124
|
+
<div className="space-y-2 text-sm">
|
|
125
|
+
<div className="flex items-center gap-2">
|
|
126
|
+
<span className="text-muted-foreground w-24 shrink-0">Provider</span>
|
|
127
|
+
<span className="font-mono text-xs">{status.provider}</span>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="flex items-center gap-2">
|
|
130
|
+
<span className="text-muted-foreground w-24 shrink-0">Remote</span>
|
|
131
|
+
<span className="font-mono text-xs truncate" title={status.remote}>{status.remote}</span>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="flex items-center gap-2">
|
|
134
|
+
<span className="text-muted-foreground w-24 shrink-0">Branch</span>
|
|
135
|
+
<span className="font-mono text-xs">{status.branch}</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div className="flex items-center gap-2">
|
|
138
|
+
<span className="text-muted-foreground w-24 shrink-0">Last sync</span>
|
|
139
|
+
<span className="text-xs">{timeAgo(status.lastSync)}</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="flex items-center gap-2">
|
|
142
|
+
<span className="text-muted-foreground w-24 shrink-0">Unpushed</span>
|
|
143
|
+
<span className="text-xs">{status.unpushed} commits</span>
|
|
144
|
+
</div>
|
|
145
|
+
<div className="flex items-center gap-2">
|
|
146
|
+
<span className="text-muted-foreground w-24 shrink-0">Auto-sync</span>
|
|
147
|
+
<span className="text-xs">
|
|
148
|
+
commit: {status.autoCommitInterval}s, pull: {Math.floor((status.autoPullInterval || 300) / 60)}min
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{/* Actions */}
|
|
154
|
+
<div className="flex items-center gap-2 pt-2">
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
onClick={handleSyncNow}
|
|
158
|
+
disabled={syncing}
|
|
159
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
160
|
+
>
|
|
161
|
+
<RefreshCw size={12} className={syncing ? 'animate-spin' : ''} />
|
|
162
|
+
Sync Now
|
|
163
|
+
</button>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={handleToggle}
|
|
167
|
+
disabled={toggling}
|
|
168
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
|
|
169
|
+
status.enabled
|
|
170
|
+
? 'border-border text-muted-foreground hover:text-destructive hover:border-destructive/50'
|
|
171
|
+
: 'border-green-500/30 text-green-500 hover:bg-green-500/10'
|
|
172
|
+
}`}
|
|
173
|
+
>
|
|
174
|
+
{status.enabled ? 'Disable Auto-sync' : 'Enable Auto-sync'}
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* Message */}
|
|
179
|
+
{message && (
|
|
180
|
+
<div className="flex items-center gap-1.5 text-xs">
|
|
181
|
+
{message.type === 'success' ? (
|
|
182
|
+
<><CheckCircle2 size={13} className="text-green-500" /><span className="text-green-500">{message.text}</span></>
|
|
183
|
+
) : (
|
|
184
|
+
<><AlertCircle size={13} className="text-destructive" /><span className="text-destructive">{message.text}</span></>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{/* Conflicts */}
|
|
190
|
+
{conflicts.length > 0 && (
|
|
191
|
+
<div className="pt-2 border-t border-border">
|
|
192
|
+
<SectionLabel>Conflicts ({conflicts.length})</SectionLabel>
|
|
193
|
+
<div className="space-y-1">
|
|
194
|
+
{conflicts.map((c, i) => (
|
|
195
|
+
<div key={i} className="flex items-center gap-2 text-xs">
|
|
196
|
+
<AlertCircle size={12} className="text-amber-500 shrink-0" />
|
|
197
|
+
<span className="font-mono truncate">{c.file}</span>
|
|
198
|
+
<span className="text-muted-foreground shrink-0">{timeAgo(c.time)}</span>
|
|
199
|
+
</div>
|
|
200
|
+
))}
|
|
201
|
+
</div>
|
|
202
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
203
|
+
Remote versions saved as <code className="font-mono">.sync-conflict</code> files.
|
|
204
|
+
</p>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{/* Error */}
|
|
209
|
+
{status.lastError && (
|
|
210
|
+
<div className="pt-2 border-t border-border">
|
|
211
|
+
<div className="flex items-start gap-2 text-xs text-destructive">
|
|
212
|
+
<AlertCircle size={12} className="shrink-0 mt-0.5" />
|
|
213
|
+
<span>{status.lastError}</span>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
@@ -24,7 +24,7 @@ export interface SettingsData {
|
|
|
24
24
|
envValues?: Record<string, string>;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export type Tab = 'ai' | 'appearance' | 'knowledge' | 'plugins' | 'shortcuts';
|
|
27
|
+
export type Tab = 'ai' | 'appearance' | 'knowledge' | 'plugins' | 'shortcuts' | 'sync';
|
|
28
28
|
|
|
29
29
|
export const CONTENT_WIDTHS = [
|
|
30
30
|
{ value: '680px', label: 'Narrow (680px)' },
|
package/app/lib/i18n.ts
CHANGED
|
@@ -83,7 +83,7 @@ export const messages = {
|
|
|
83
83
|
},
|
|
84
84
|
settings: {
|
|
85
85
|
title: 'Settings',
|
|
86
|
-
tabs: { ai: 'AI', appearance: 'Appearance', knowledge: 'Knowledge Base', plugins: 'Plugins', shortcuts: 'Shortcuts' },
|
|
86
|
+
tabs: { ai: 'AI', appearance: 'Appearance', knowledge: 'Knowledge Base', sync: 'Sync', plugins: 'Plugins', shortcuts: 'Shortcuts' },
|
|
87
87
|
ai: {
|
|
88
88
|
provider: 'Provider',
|
|
89
89
|
model: 'Model',
|
|
@@ -230,7 +230,7 @@ export const messages = {
|
|
|
230
230
|
},
|
|
231
231
|
settings: {
|
|
232
232
|
title: '设置',
|
|
233
|
-
tabs: { ai: 'AI', appearance: '外观', knowledge: '知识库', plugins: '插件', shortcuts: '快捷键' },
|
|
233
|
+
tabs: { ai: 'AI', appearance: '外观', knowledge: '知识库', sync: '同步', plugins: '插件', shortcuts: '快捷键' },
|
|
234
234
|
ai: {
|
|
235
235
|
provider: '服务商',
|
|
236
236
|
model: '模型',
|