@geminilight/mindos 0.1.9 → 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 +39 -9
- 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/bin/cli.js +193 -10
- package/bin/lib/config.js +12 -1
- package/bin/lib/sync.js +367 -0
- package/package.json +6 -2
- package/scripts/release.sh +56 -0
- package/scripts/setup.js +23 -5
package/bin/cli.js
CHANGED
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
* mindos mcp — start MCP server only
|
|
16
16
|
* mindos stop — stop running MindOS processes
|
|
17
17
|
* mindos restart — stop then start
|
|
18
|
+
* mindos open — open Web UI in the default browser
|
|
18
19
|
* mindos token — show current auth token and MCP config snippet
|
|
20
|
+
* mindos sync — show sync status
|
|
21
|
+
* mindos sync init — configure remote git repo for sync
|
|
22
|
+
* mindos sync now — manual trigger sync
|
|
23
|
+
* mindos sync conflicts — list conflict files
|
|
24
|
+
* mindos sync on|off — enable/disable auto-sync
|
|
19
25
|
* mindos gateway install — install background service (systemd/launchd)
|
|
20
26
|
* mindos gateway uninstall — remove background service
|
|
21
27
|
* mindos gateway start — start the background service
|
|
@@ -38,20 +44,21 @@ import { homedir } from 'node:os';
|
|
|
38
44
|
import { ROOT, CONFIG_PATH, BUILD_STAMP, LOG_PATH } from './lib/constants.js';
|
|
39
45
|
import { bold, dim, cyan, green, red, yellow } from './lib/colors.js';
|
|
40
46
|
import { run } from './lib/utils.js';
|
|
41
|
-
import { loadConfig, getStartMode } from './lib/config.js';
|
|
47
|
+
import { loadConfig, getStartMode, isDaemonMode } from './lib/config.js';
|
|
42
48
|
import { needsBuild, writeBuildStamp, clearBuildLock, cleanNextDir, ensureAppDeps } from './lib/build.js';
|
|
43
49
|
import { isPortInUse, assertPortFree } from './lib/port.js';
|
|
44
50
|
import { savePids, clearPids } from './lib/pid.js';
|
|
45
51
|
import { stopMindos } from './lib/stop.js';
|
|
46
52
|
import { getPlatform, ensureMindosDir, waitForHttp, runGatewayCommand } from './lib/gateway.js';
|
|
47
|
-
import { printStartupInfo } from './lib/startup.js';
|
|
53
|
+
import { printStartupInfo, getLocalIP } from './lib/startup.js';
|
|
48
54
|
import { spawnMcp } from './lib/mcp-spawn.js';
|
|
49
55
|
import { mcpInstall } from './lib/mcp-install.js';
|
|
56
|
+
import { initSync, startSyncDaemon, stopSyncDaemon, getSyncStatus, manualSync, listConflicts, setSyncEnabled } from './lib/sync.js';
|
|
50
57
|
|
|
51
58
|
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
52
59
|
|
|
53
60
|
const cmd = process.argv[2];
|
|
54
|
-
const isDaemon = process.argv.includes('--daemon');
|
|
61
|
+
const isDaemon = process.argv.includes('--daemon') || (!cmd && isDaemonMode());
|
|
55
62
|
const isVerbose = process.argv.includes('--verbose');
|
|
56
63
|
const extra = process.argv.slice(3).filter(a => a !== '--daemon' && a !== '--verbose').join(' ');
|
|
57
64
|
|
|
@@ -64,22 +71,117 @@ const commands = {
|
|
|
64
71
|
init: () => run(`node ${resolve(ROOT, 'scripts/setup.js')}`),
|
|
65
72
|
setup: () => run(`node ${resolve(ROOT, 'scripts/setup.js')}`),
|
|
66
73
|
|
|
74
|
+
// ── open ───────────────────────────────────────────────────────────────────
|
|
75
|
+
open: () => {
|
|
76
|
+
loadConfig();
|
|
77
|
+
const webPort = process.env.MINDOS_WEB_PORT || '3000';
|
|
78
|
+
const url = `http://localhost:${webPort}`;
|
|
79
|
+
let cmd;
|
|
80
|
+
if (process.platform === 'darwin') {
|
|
81
|
+
cmd = 'open';
|
|
82
|
+
} else if (process.platform === 'linux') {
|
|
83
|
+
// WSL detection
|
|
84
|
+
try {
|
|
85
|
+
const uname = execSync('uname -r', { encoding: 'utf-8' });
|
|
86
|
+
cmd = uname.toLowerCase().includes('microsoft') ? 'wslview' : 'xdg-open';
|
|
87
|
+
} catch {
|
|
88
|
+
cmd = 'xdg-open';
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
cmd = 'start';
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
execSync(`${cmd} ${url}`, { stdio: 'ignore' });
|
|
95
|
+
console.log(`${green('✔')} Opening ${cyan(url)}`);
|
|
96
|
+
} catch {
|
|
97
|
+
console.log(dim(`Could not open browser automatically. Visit: ${cyan(url)}`));
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
67
101
|
// ── token ──────────────────────────────────────────────────────────────────
|
|
68
102
|
token: () => {
|
|
69
103
|
if (!existsSync(CONFIG_PATH)) {
|
|
70
104
|
console.error(red('No config found. Run `mindos onboard` first.'));
|
|
71
105
|
process.exit(1);
|
|
72
106
|
}
|
|
73
|
-
let
|
|
74
|
-
try {
|
|
107
|
+
let config = {};
|
|
108
|
+
try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
|
|
109
|
+
const token = config.authToken || '';
|
|
75
110
|
if (!token) {
|
|
76
111
|
console.log(dim('No auth token set. Run `mindos onboard` to configure one.'));
|
|
77
112
|
process.exit(0);
|
|
78
113
|
}
|
|
114
|
+
const mcpPort = config.mcpPort || 8787;
|
|
115
|
+
const localIP = getLocalIP();
|
|
116
|
+
|
|
117
|
+
const localUrl = `http://localhost:${mcpPort}/mcp`;
|
|
118
|
+
const sep = '━'.repeat(40);
|
|
119
|
+
|
|
79
120
|
console.log(`\n${bold('🔑 Auth token:')} ${cyan(token)}\n`);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.log(
|
|
121
|
+
|
|
122
|
+
// Claude Code
|
|
123
|
+
console.log(`${sep}`);
|
|
124
|
+
console.log(`${bold('Claude Code')}`);
|
|
125
|
+
console.log(`${sep}`);
|
|
126
|
+
console.log(dim('一键安装:') + ` mindos mcp install claude-code -g -y`);
|
|
127
|
+
console.log(dim('\n手动配置 (~/.claude.json):'));
|
|
128
|
+
console.log(JSON.stringify({
|
|
129
|
+
mcpServers: {
|
|
130
|
+
mindos: {
|
|
131
|
+
url: localUrl,
|
|
132
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}, null, 2));
|
|
136
|
+
|
|
137
|
+
// CodeBuddy (Claude Code Internal)
|
|
138
|
+
console.log(`\n${sep}`);
|
|
139
|
+
console.log(`${bold('CodeBuddy (Claude Code Internal)')}`);
|
|
140
|
+
console.log(`${sep}`);
|
|
141
|
+
console.log(dim('一键安装:') + ` mindos mcp install codebuddy -g -y`);
|
|
142
|
+
console.log(dim('\n手动配置 (~/.claude-internal/.claude.json):'));
|
|
143
|
+
console.log(JSON.stringify({
|
|
144
|
+
mcpServers: {
|
|
145
|
+
mindos: {
|
|
146
|
+
url: localUrl,
|
|
147
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}, null, 2));
|
|
151
|
+
|
|
152
|
+
// Cursor
|
|
153
|
+
console.log(`\n${sep}`);
|
|
154
|
+
console.log(`${bold('Cursor')}`);
|
|
155
|
+
console.log(`${sep}`);
|
|
156
|
+
console.log(dim('一键安装:') + ` mindos mcp install cursor -g -y`);
|
|
157
|
+
console.log(dim('\n手动配置 (~/.cursor/mcp.json):'));
|
|
158
|
+
console.log(JSON.stringify({
|
|
159
|
+
mcpServers: {
|
|
160
|
+
mindos: {
|
|
161
|
+
url: localUrl,
|
|
162
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
}, null, 2));
|
|
166
|
+
|
|
167
|
+
// Remote
|
|
168
|
+
if (localIP) {
|
|
169
|
+
const remoteUrl = `http://${localIP}:${mcpPort}/mcp`;
|
|
170
|
+
console.log(`\n${sep}`);
|
|
171
|
+
console.log(`${bold('Remote (其他设备)')}`);
|
|
172
|
+
console.log(`${sep}`);
|
|
173
|
+
console.log(`URL: ${cyan(remoteUrl)}`);
|
|
174
|
+
console.log(JSON.stringify({
|
|
175
|
+
mcpServers: {
|
|
176
|
+
mindos: {
|
|
177
|
+
url: remoteUrl,
|
|
178
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
}, null, 2));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(dim('\nRun `mindos onboard` to regenerate.\n'));
|
|
83
185
|
},
|
|
84
186
|
|
|
85
187
|
// ── dev ────────────────────────────────────────────────────────────────────
|
|
@@ -92,7 +194,12 @@ const commands = {
|
|
|
92
194
|
ensureAppDeps();
|
|
93
195
|
const mcp = spawnMcp(isVerbose);
|
|
94
196
|
savePids(process.pid, mcp.pid);
|
|
95
|
-
process.on('exit', clearPids);
|
|
197
|
+
process.on('exit', () => { stopSyncDaemon(); clearPids(); });
|
|
198
|
+
// Start sync daemon if enabled
|
|
199
|
+
const devMindRoot = process.env.MIND_ROOT;
|
|
200
|
+
if (devMindRoot) {
|
|
201
|
+
startSyncDaemon(devMindRoot).catch(() => {});
|
|
202
|
+
}
|
|
96
203
|
printStartupInfo(webPort, mcpPort);
|
|
97
204
|
run(`npx next dev -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
|
|
98
205
|
},
|
|
@@ -119,6 +226,14 @@ const commands = {
|
|
|
119
226
|
process.exit(1);
|
|
120
227
|
}
|
|
121
228
|
printStartupInfo(webPort, mcpPort);
|
|
229
|
+
// System notification
|
|
230
|
+
try {
|
|
231
|
+
if (process.platform === 'darwin') {
|
|
232
|
+
execSync(`osascript -e 'display notification "http://localhost:${webPort}" with title "MindOS 已就绪"'`, { stdio: 'ignore' });
|
|
233
|
+
} else if (process.platform === 'linux') {
|
|
234
|
+
execSync(`notify-send "MindOS 已就绪" "http://localhost:${webPort}"`, { stdio: 'ignore' });
|
|
235
|
+
}
|
|
236
|
+
} catch { /* notification is best-effort */ }
|
|
122
237
|
console.log(`${green('✔ MindOS is running as a background service')}`);
|
|
123
238
|
console.log(dim(' View logs: mindos logs'));
|
|
124
239
|
console.log(dim(' Stop: mindos gateway stop'));
|
|
@@ -140,7 +255,12 @@ const commands = {
|
|
|
140
255
|
}
|
|
141
256
|
const mcp = spawnMcp(isVerbose);
|
|
142
257
|
savePids(process.pid, mcp.pid);
|
|
143
|
-
process.on('exit', clearPids);
|
|
258
|
+
process.on('exit', () => { stopSyncDaemon(); clearPids(); });
|
|
259
|
+
// Start sync daemon if enabled
|
|
260
|
+
const mindRoot = process.env.MIND_ROOT;
|
|
261
|
+
if (mindRoot) {
|
|
262
|
+
startSyncDaemon(mindRoot).catch(() => {});
|
|
263
|
+
}
|
|
144
264
|
printStartupInfo(webPort, mcpPort);
|
|
145
265
|
run(`npx next start -p ${webPort} ${extra}`, resolve(ROOT, 'app'));
|
|
146
266
|
},
|
|
@@ -521,6 +641,67 @@ ${bold('Examples:')}
|
|
|
521
641
|
${dim('mindos config set ai.provider openai')}
|
|
522
642
|
`);
|
|
523
643
|
},
|
|
644
|
+
|
|
645
|
+
// ── sync ──────────────────────────────────────────────────────────────────
|
|
646
|
+
sync: async () => {
|
|
647
|
+
const sub = process.argv[3];
|
|
648
|
+
loadConfig();
|
|
649
|
+
const mindRoot = process.env.MIND_ROOT;
|
|
650
|
+
|
|
651
|
+
if (sub === 'init') {
|
|
652
|
+
await initSync(mindRoot);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (sub === 'now') {
|
|
657
|
+
manualSync(mindRoot);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (sub === 'conflicts') {
|
|
662
|
+
listConflicts(mindRoot);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (sub === 'on') {
|
|
667
|
+
setSyncEnabled(true);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (sub === 'off') {
|
|
672
|
+
setSyncEnabled(false);
|
|
673
|
+
stopSyncDaemon();
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// default: sync status
|
|
678
|
+
const status = getSyncStatus(mindRoot);
|
|
679
|
+
if (!status.enabled) {
|
|
680
|
+
console.log(`\n${bold('🔄 Sync Status')}`);
|
|
681
|
+
console.log(dim(' Not configured. Run `mindos sync init` to set up.\n'));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const ago = status.lastSync
|
|
685
|
+
? (() => {
|
|
686
|
+
const diff = Date.now() - new Date(status.lastSync).getTime();
|
|
687
|
+
if (diff < 60000) return 'just now';
|
|
688
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)} minutes ago`;
|
|
689
|
+
return `${Math.floor(diff / 3600000)} hours ago`;
|
|
690
|
+
})()
|
|
691
|
+
: 'never';
|
|
692
|
+
|
|
693
|
+
console.log(`\n${bold('🔄 Sync Status')}`);
|
|
694
|
+
console.log(` ${dim('Provider:')} ${cyan(`${status.provider} (${status.remote})`)}`);
|
|
695
|
+
console.log(` ${dim('Branch:')} ${cyan(status.branch)}`);
|
|
696
|
+
console.log(` ${dim('Last sync:')} ${ago}`);
|
|
697
|
+
console.log(` ${dim('Unpushed:')} ${status.unpushed} commits`);
|
|
698
|
+
console.log(` ${dim('Conflicts:')} ${status.conflicts.length ? yellow(`${status.conflicts.length} file(s)`) : green('none')}`);
|
|
699
|
+
console.log(` ${dim('Auto-sync:')} ${green('● enabled')} ${dim(`(commit: ${status.autoCommitInterval}s, pull: ${status.autoPullInterval / 60}min)`)}`);
|
|
700
|
+
if (status.lastError) {
|
|
701
|
+
console.log(` ${dim('Last error:')} ${red(status.lastError)}`);
|
|
702
|
+
}
|
|
703
|
+
console.log();
|
|
704
|
+
},
|
|
524
705
|
};
|
|
525
706
|
|
|
526
707
|
// ── Entry ─────────────────────────────────────────────────────────────────────
|
|
@@ -545,7 +726,9 @@ ${row('mindos restart', 'Stop then start again')}
|
|
|
545
726
|
${row('mindos build', 'Build the app for production')}
|
|
546
727
|
${row('mindos mcp', 'Start MCP server only')}
|
|
547
728
|
${row('mindos mcp install [agent]', 'Install MindOS MCP config into Agent (claude-code/cursor/windsurf/…) [-g]')}
|
|
729
|
+
${row('mindos open', 'Open Web UI in the default browser')}
|
|
548
730
|
${row('mindos token', 'Show current auth token and MCP config snippet')}
|
|
731
|
+
${row('mindos sync', 'Show sync status (init/now/conflicts/on/off)')}
|
|
549
732
|
${row('mindos gateway <subcommand>', 'Manage background service (install/uninstall/start/stop/status/logs)')}
|
|
550
733
|
${row('mindos doctor', 'Health check (config, ports, build, daemon)')}
|
|
551
734
|
${row('mindos update', 'Update MindOS to the latest version')}
|
package/bin/lib/config.js
CHANGED
|
@@ -40,8 +40,19 @@ export function loadConfig() {
|
|
|
40
40
|
|
|
41
41
|
export function getStartMode() {
|
|
42
42
|
try {
|
|
43
|
-
|
|
43
|
+
const mode = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).startMode || 'start';
|
|
44
|
+
// 'daemon' is stored in config when user chose background service;
|
|
45
|
+
// CLI maps it to the 'start' command with --daemon flag
|
|
46
|
+
return mode === 'daemon' ? 'start' : mode;
|
|
44
47
|
} catch {
|
|
45
48
|
return 'start';
|
|
46
49
|
}
|
|
47
50
|
}
|
|
51
|
+
|
|
52
|
+
export function isDaemonMode() {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')).startMode === 'daemon';
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
package/bin/lib/sync.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { CONFIG_PATH, MINDOS_DIR } from './constants.js';
|
|
5
|
+
import { bold, dim, cyan, green, red, yellow } from './colors.js';
|
|
6
|
+
|
|
7
|
+
// ── Config helpers ──────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
function loadSyncConfig() {
|
|
10
|
+
try {
|
|
11
|
+
const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
12
|
+
return config.sync || {};
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function saveSyncConfig(syncConfig) {
|
|
19
|
+
let config = {};
|
|
20
|
+
try { config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')); } catch {}
|
|
21
|
+
config.sync = syncConfig;
|
|
22
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getMindRoot() {
|
|
26
|
+
try {
|
|
27
|
+
const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
28
|
+
return config.mindRoot;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SYNC_STATE_PATH = resolve(MINDOS_DIR, 'sync-state.json');
|
|
35
|
+
|
|
36
|
+
function loadSyncState() {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(SYNC_STATE_PATH, 'utf-8'));
|
|
39
|
+
} catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function saveSyncState(state) {
|
|
45
|
+
if (!existsSync(MINDOS_DIR)) mkdirSync(MINDOS_DIR, { recursive: true });
|
|
46
|
+
writeFileSync(SYNC_STATE_PATH, JSON.stringify(state, null, 2) + '\n', 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Git helpers ─────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function isGitRepo(dir) {
|
|
52
|
+
return existsSync(resolve(dir, '.git'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function gitExec(cmd, cwd) {
|
|
56
|
+
return execSync(cmd, { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getRemoteUrl(cwd) {
|
|
60
|
+
try {
|
|
61
|
+
return gitExec('git remote get-url origin', cwd);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getBranch(cwd) {
|
|
68
|
+
try {
|
|
69
|
+
return gitExec('git rev-parse --abbrev-ref HEAD', cwd);
|
|
70
|
+
} catch {
|
|
71
|
+
return 'main';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getUnpushedCount(cwd) {
|
|
76
|
+
try {
|
|
77
|
+
return gitExec('git rev-list --count @{u}..HEAD', cwd);
|
|
78
|
+
} catch {
|
|
79
|
+
return '?';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Core sync functions ─────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function autoCommitAndPush(mindRoot) {
|
|
86
|
+
try {
|
|
87
|
+
execSync('git add -A', { cwd: mindRoot, stdio: 'pipe' });
|
|
88
|
+
const status = gitExec('git status --porcelain', mindRoot);
|
|
89
|
+
if (!status) return;
|
|
90
|
+
const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
91
|
+
execSync(`git commit -m "auto-sync: ${timestamp}"`, { cwd: mindRoot, stdio: 'pipe' });
|
|
92
|
+
execSync('git push', { cwd: mindRoot, stdio: 'pipe' });
|
|
93
|
+
saveSyncState({ ...loadSyncState(), lastSync: new Date().toISOString(), lastError: null });
|
|
94
|
+
} catch (err) {
|
|
95
|
+
saveSyncState({ ...loadSyncState(), lastError: err.message, lastErrorTime: new Date().toISOString() });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function autoPull(mindRoot) {
|
|
100
|
+
try {
|
|
101
|
+
execSync('git pull --rebase --autostash', { cwd: mindRoot, stdio: 'pipe' });
|
|
102
|
+
saveSyncState({ ...loadSyncState(), lastPull: new Date().toISOString() });
|
|
103
|
+
} catch {
|
|
104
|
+
// rebase conflict → abort → merge
|
|
105
|
+
try { execSync('git rebase --abort', { cwd: mindRoot, stdio: 'pipe' }); } catch {}
|
|
106
|
+
try {
|
|
107
|
+
execSync('git pull --no-rebase', { cwd: mindRoot, stdio: 'pipe' });
|
|
108
|
+
saveSyncState({ ...loadSyncState(), lastPull: new Date().toISOString() });
|
|
109
|
+
} catch {
|
|
110
|
+
// merge conflict → keep both versions
|
|
111
|
+
try {
|
|
112
|
+
const conflicts = gitExec('git diff --name-only --diff-filter=U', mindRoot).split('\n').filter(Boolean);
|
|
113
|
+
for (const file of conflicts) {
|
|
114
|
+
try {
|
|
115
|
+
const theirs = execSync(`git show :3:${file}`, { cwd: mindRoot, encoding: 'utf-8' });
|
|
116
|
+
writeFileSync(resolve(mindRoot, file + '.sync-conflict'), theirs, 'utf-8');
|
|
117
|
+
} catch {}
|
|
118
|
+
try { execSync(`git checkout --ours "${file}"`, { cwd: mindRoot, stdio: 'pipe' }); } catch {}
|
|
119
|
+
}
|
|
120
|
+
execSync('git add -A', { cwd: mindRoot, stdio: 'pipe' });
|
|
121
|
+
execSync('git commit -m "auto-sync: resolved conflicts (kept both versions)"', { cwd: mindRoot, stdio: 'pipe' });
|
|
122
|
+
saveSyncState({
|
|
123
|
+
...loadSyncState(),
|
|
124
|
+
lastPull: new Date().toISOString(),
|
|
125
|
+
conflicts: conflicts.map(f => ({ file: f, time: new Date().toISOString() })),
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
saveSyncState({ ...loadSyncState(), lastError: err.message, lastErrorTime: new Date().toISOString() });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Exported API ────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
let activeWatcher = null;
|
|
137
|
+
let activePullInterval = null;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Interactive sync init — configure remote git repo
|
|
141
|
+
*/
|
|
142
|
+
export async function initSync(mindRoot) {
|
|
143
|
+
if (!mindRoot) { console.error(red('No mindRoot configured.')); process.exit(1); }
|
|
144
|
+
|
|
145
|
+
const readline = await import('node:readline');
|
|
146
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
147
|
+
const ask = (q) => new Promise(r => rl.question(q, r));
|
|
148
|
+
|
|
149
|
+
// 1. Ensure git repo
|
|
150
|
+
if (!isGitRepo(mindRoot)) {
|
|
151
|
+
console.log(dim('Initializing git repository...'));
|
|
152
|
+
execSync('git init', { cwd: mindRoot, stdio: 'inherit' });
|
|
153
|
+
execSync('git checkout -b main', { cwd: mindRoot, stdio: 'pipe' }).toString();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 2. Remote URL
|
|
157
|
+
const currentRemote = getRemoteUrl(mindRoot);
|
|
158
|
+
const defaultUrl = currentRemote || '';
|
|
159
|
+
const urlPrompt = currentRemote
|
|
160
|
+
? `${bold('Remote URL')} ${dim(`[${currentRemote}]`)}: `
|
|
161
|
+
: `${bold('Remote URL')} ${dim('(HTTPS or SSH)')}: `;
|
|
162
|
+
let remoteUrl = (await ask(urlPrompt)).trim() || defaultUrl;
|
|
163
|
+
|
|
164
|
+
if (!remoteUrl) {
|
|
165
|
+
console.error(red('Remote URL is required.'));
|
|
166
|
+
rl.close();
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3. Token for HTTPS
|
|
171
|
+
let token = '';
|
|
172
|
+
if (remoteUrl.startsWith('https://')) {
|
|
173
|
+
token = (await ask(`${bold('Access Token')} ${dim('(GitHub PAT / GitLab PAT, leave empty if SSH)')}: `)).trim();
|
|
174
|
+
if (token) {
|
|
175
|
+
// Inject token into URL for credential storage
|
|
176
|
+
const urlObj = new URL(remoteUrl);
|
|
177
|
+
urlObj.username = 'oauth2';
|
|
178
|
+
urlObj.password = token;
|
|
179
|
+
const authUrl = urlObj.toString();
|
|
180
|
+
// Configure credential helper
|
|
181
|
+
try { execSync(`git config credential.helper store`, { cwd: mindRoot, stdio: 'pipe' }); } catch {}
|
|
182
|
+
// Store the credential
|
|
183
|
+
try {
|
|
184
|
+
const credInput = `protocol=${urlObj.protocol.replace(':', '')}\nhost=${urlObj.host}\nusername=oauth2\npassword=${token}\n\n`;
|
|
185
|
+
execSync('git credential approve', { cwd: mindRoot, input: credInput, stdio: 'pipe' });
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 4. Set remote
|
|
191
|
+
try {
|
|
192
|
+
execSync(`git remote add origin "${remoteUrl}"`, { cwd: mindRoot, stdio: 'pipe' });
|
|
193
|
+
} catch {
|
|
194
|
+
execSync(`git remote set-url origin "${remoteUrl}"`, { cwd: mindRoot, stdio: 'pipe' });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 5. Test connection
|
|
198
|
+
console.log(dim('Testing connection...'));
|
|
199
|
+
try {
|
|
200
|
+
execSync('git ls-remote --exit-code origin', { cwd: mindRoot, stdio: 'pipe' });
|
|
201
|
+
console.log(green('✔ Connection successful'));
|
|
202
|
+
} catch {
|
|
203
|
+
console.error(red('✘ Could not connect to remote. Check your URL and credentials.'));
|
|
204
|
+
rl.close();
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
rl.close();
|
|
209
|
+
|
|
210
|
+
// 6. Save sync config
|
|
211
|
+
const syncConfig = {
|
|
212
|
+
enabled: true,
|
|
213
|
+
provider: 'git',
|
|
214
|
+
remote: 'origin',
|
|
215
|
+
branch: getBranch(mindRoot),
|
|
216
|
+
autoCommitInterval: 30,
|
|
217
|
+
autoPullInterval: 300,
|
|
218
|
+
};
|
|
219
|
+
saveSyncConfig(syncConfig);
|
|
220
|
+
console.log(green('✔ Sync configured'));
|
|
221
|
+
|
|
222
|
+
// 7. First sync: pull if remote has content, push otherwise
|
|
223
|
+
try {
|
|
224
|
+
const refs = gitExec('git ls-remote --heads origin', mindRoot);
|
|
225
|
+
if (refs) {
|
|
226
|
+
console.log(dim('Pulling from remote...'));
|
|
227
|
+
try {
|
|
228
|
+
execSync(`git pull origin ${syncConfig.branch} --allow-unrelated-histories`, { cwd: mindRoot, stdio: 'inherit' });
|
|
229
|
+
} catch {
|
|
230
|
+
// Might fail if empty or conflicts — that's fine for initial setup
|
|
231
|
+
console.log(yellow('Pull completed with warnings. Check for conflicts.'));
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
console.log(dim('Pushing to remote...'));
|
|
235
|
+
autoCommitAndPush(mindRoot);
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
console.log(dim('Performing initial push...'));
|
|
239
|
+
autoCommitAndPush(mindRoot);
|
|
240
|
+
}
|
|
241
|
+
console.log(green('✔ Initial sync complete\n'));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Start file watcher + periodic pull
|
|
246
|
+
*/
|
|
247
|
+
export async function startSyncDaemon(mindRoot) {
|
|
248
|
+
const config = loadSyncConfig();
|
|
249
|
+
if (!config.enabled) return null;
|
|
250
|
+
if (!mindRoot || !isGitRepo(mindRoot)) return null;
|
|
251
|
+
|
|
252
|
+
const chokidar = await import('chokidar');
|
|
253
|
+
|
|
254
|
+
// File watcher → debounced auto-commit + push
|
|
255
|
+
let commitTimer = null;
|
|
256
|
+
const watcher = chokidar.watch(mindRoot, {
|
|
257
|
+
ignored: [/(^|[/\\])\.git/, /node_modules/, /\.sync-conflict$/],
|
|
258
|
+
persistent: true,
|
|
259
|
+
ignoreInitial: true,
|
|
260
|
+
});
|
|
261
|
+
watcher.on('all', () => {
|
|
262
|
+
clearTimeout(commitTimer);
|
|
263
|
+
commitTimer = setTimeout(() => autoCommitAndPush(mindRoot), (config.autoCommitInterval || 30) * 1000);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Periodic pull
|
|
267
|
+
const pullInterval = setInterval(() => autoPull(mindRoot), (config.autoPullInterval || 300) * 1000);
|
|
268
|
+
|
|
269
|
+
// Pull on startup
|
|
270
|
+
autoPull(mindRoot);
|
|
271
|
+
|
|
272
|
+
activeWatcher = watcher;
|
|
273
|
+
activePullInterval = pullInterval;
|
|
274
|
+
|
|
275
|
+
return { watcher, pullInterval };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Stop sync daemon
|
|
280
|
+
*/
|
|
281
|
+
export function stopSyncDaemon() {
|
|
282
|
+
if (activeWatcher) {
|
|
283
|
+
activeWatcher.close();
|
|
284
|
+
activeWatcher = null;
|
|
285
|
+
}
|
|
286
|
+
if (activePullInterval) {
|
|
287
|
+
clearInterval(activePullInterval);
|
|
288
|
+
activePullInterval = null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get current sync status
|
|
294
|
+
*/
|
|
295
|
+
export function getSyncStatus(mindRoot) {
|
|
296
|
+
const config = loadSyncConfig();
|
|
297
|
+
const state = loadSyncState();
|
|
298
|
+
|
|
299
|
+
if (!config.enabled) {
|
|
300
|
+
return { enabled: false };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const remote = mindRoot ? getRemoteUrl(mindRoot) : null;
|
|
304
|
+
const branch = mindRoot ? getBranch(mindRoot) : null;
|
|
305
|
+
const unpushed = mindRoot ? getUnpushedCount(mindRoot) : '?';
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
enabled: true,
|
|
309
|
+
provider: config.provider || 'git',
|
|
310
|
+
remote: remote || '(not configured)',
|
|
311
|
+
branch: branch || 'main',
|
|
312
|
+
lastSync: state.lastSync || null,
|
|
313
|
+
lastPull: state.lastPull || null,
|
|
314
|
+
unpushed,
|
|
315
|
+
conflicts: state.conflicts || [],
|
|
316
|
+
lastError: state.lastError || null,
|
|
317
|
+
autoCommitInterval: config.autoCommitInterval || 30,
|
|
318
|
+
autoPullInterval: config.autoPullInterval || 300,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Manual trigger of full sync cycle
|
|
324
|
+
*/
|
|
325
|
+
export function manualSync(mindRoot) {
|
|
326
|
+
if (!mindRoot || !isGitRepo(mindRoot)) {
|
|
327
|
+
console.error(red('Not a git repository. Run `mindos sync init` first.'));
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
console.log(dim('Pulling...'));
|
|
331
|
+
autoPull(mindRoot);
|
|
332
|
+
console.log(dim('Committing & pushing...'));
|
|
333
|
+
autoCommitAndPush(mindRoot);
|
|
334
|
+
console.log(green('✔ Sync complete'));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* List conflict files
|
|
339
|
+
*/
|
|
340
|
+
export function listConflicts(mindRoot) {
|
|
341
|
+
const state = loadSyncState();
|
|
342
|
+
const conflicts = state.conflicts || [];
|
|
343
|
+
if (!conflicts.length) {
|
|
344
|
+
console.log(green('No conflicts'));
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
console.log(bold(`${conflicts.length} conflict(s):\n`));
|
|
348
|
+
for (const c of conflicts) {
|
|
349
|
+
console.log(` ${yellow('●')} ${c.file} ${dim(c.time)}`);
|
|
350
|
+
const conflictPath = resolve(mindRoot, c.file + '.sync-conflict');
|
|
351
|
+
if (existsSync(conflictPath)) {
|
|
352
|
+
console.log(dim(` Remote version saved: ${c.file}.sync-conflict`));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
console.log();
|
|
356
|
+
return conflicts;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Enable/disable sync
|
|
361
|
+
*/
|
|
362
|
+
export function setSyncEnabled(enabled) {
|
|
363
|
+
const config = loadSyncConfig();
|
|
364
|
+
config.enabled = enabled;
|
|
365
|
+
saveSyncConfig(config);
|
|
366
|
+
console.log(enabled ? green('✔ Auto-sync enabled') : yellow('Auto-sync disabled'));
|
|
367
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geminilight/mindos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mindos",
|
|
@@ -51,9 +51,13 @@
|
|
|
51
51
|
"build": "mindos build",
|
|
52
52
|
"start": "mindos start",
|
|
53
53
|
"mcp": "mindos mcp",
|
|
54
|
-
"test": "cd app && npx vitest run"
|
|
54
|
+
"test": "cd app && npx vitest run",
|
|
55
|
+
"release": "bash scripts/release.sh"
|
|
55
56
|
},
|
|
56
57
|
"engines": {
|
|
57
58
|
"node": ">=18"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"chokidar": "^5.0.0"
|
|
58
62
|
}
|
|
59
63
|
}
|