openclaw-sync-assistant 0.0.11 → 0.1.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/index.js CHANGED
@@ -7,6 +7,12 @@ const {
7
7
  spinner,
8
8
  } = require("@clack/prompts");
9
9
  const pc = require("picocolors");
10
+ const path = require("path");
11
+ const os = require("os");
12
+ const GitSyncService = require("./src/github-sync");
13
+
14
+ let isWizardRunning = false;
15
+ let gitSyncInstance = null;
10
16
 
11
17
  module.exports = {
12
18
  /**
@@ -18,20 +24,69 @@ module.exports = {
18
24
  console.log("✅ openclaw-sync-assistant 插件已激活!");
19
25
  }
20
26
 
27
+ // 防止在 OpenClaw 的某些生命周期中 activate 被并发/多次调用导致向导重复弹出
28
+ if (isWizardRunning) return;
29
+
21
30
  // 检查是否已经配置过
22
31
  let hasConfigured = false;
32
+ let syncMethod = null;
33
+ let githubRepo = null;
34
+ let syncMode = null;
35
+
23
36
  if (context.api && context.api.config && context.api.config.get) {
24
- const currentMethod = await context.api.config.get(
37
+ syncMethod = await context.api.config.get(
25
38
  "openclaw-sync-assistant.syncMethod",
26
39
  );
27
- if (currentMethod) {
40
+ githubRepo = await context.api.config.get(
41
+ "openclaw-sync-assistant.githubRepo",
42
+ );
43
+ syncMode = await context.api.config.get(
44
+ "openclaw-sync-assistant.syncMode",
45
+ );
46
+ if (syncMethod) {
28
47
  hasConfigured = true;
29
48
  }
30
49
  }
31
50
 
32
51
  // 如果未配置,则在安装/首次加载时触发引导向导
33
52
  if (!hasConfigured) {
34
- await module.exports.runSetupWizard(context);
53
+ isWizardRunning = true;
54
+ try {
55
+ await module.exports.runSetupWizard(context);
56
+ // 向导结束后重新获取最新配置
57
+ if (context.api && context.api.config && context.api.config.get) {
58
+ syncMethod = await context.api.config.get(
59
+ "openclaw-sync-assistant.syncMethod",
60
+ );
61
+ githubRepo = await context.api.config.get(
62
+ "openclaw-sync-assistant.githubRepo",
63
+ );
64
+ syncMode = await context.api.config.get(
65
+ "openclaw-sync-assistant.syncMode",
66
+ );
67
+ }
68
+ } finally {
69
+ isWizardRunning = false;
70
+ }
71
+ }
72
+
73
+ // 启动实际的同步服务
74
+ if (syncMethod === "github" && githubRepo) {
75
+ const openclawDir = path.join(os.homedir(), ".openclaw");
76
+ // 这里我们为了安全,默认只同步 config 和 workspace 等子目录,但为了简单起见,我们在 openclawDir 下创建一个 sync 专用文件夹
77
+ // 或者直接同步整个 .openclaw 目录(需要在 github-sync.js 中排除一些不必要的缓存)
78
+ // 推荐做法:在 .openclaw/sync-data 目录做软链接或者单独管理
79
+ const syncDir = path.join(openclawDir, "sync-data");
80
+
81
+ gitSyncInstance = new GitSyncService(
82
+ syncDir,
83
+ githubRepo,
84
+ syncMode,
85
+ process.env.DEBUG === "openclaw:sync",
86
+ );
87
+ await gitSyncInstance.init();
88
+ } else if (syncMethod === "p2p") {
89
+ console.log("[OpenClaw Sync] P2P 模式尚未在此版本中完全实现核心逻辑。");
35
90
  }
36
91
  },
37
92
 
@@ -156,9 +211,13 @@ module.exports = {
156
211
  },
157
212
 
158
213
  /**
159
- * 插件卸载时的清理函数
214
+ * 插件卸载或停用时的清理函数
160
215
  */
161
216
  deactivate() {
162
- console.log("❌ openclaw-sync-assistant 插件已卸载。");
217
+ console.log("❌ openclaw-sync-assistant 插件已卸载/停用。");
218
+ if (gitSyncInstance) {
219
+ gitSyncInstance.stop();
220
+ gitSyncInstance = null;
221
+ }
163
222
  },
164
223
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openclaw-sync-assistant",
3
3
  "name": "openclaw-sync-assistant",
4
- "version": "0.0.11",
4
+ "version": "0.1.0",
5
5
  "description": "OpenClaw Sync Assistant Plugin",
6
6
  "commands": [
7
7
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-sync-assistant",
3
- "version": "0.0.11",
3
+ "version": "0.1.0",
4
4
  "description": "An OpenClaw plugin for P2P file and data synchronization using Hyperswarm and Hyperdrive ecosystem.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -44,11 +44,13 @@
44
44
  "dependencies": {
45
45
  "@clack/prompts": "^0.9.0",
46
46
  "b4a": "^1.6.6",
47
+ "chokidar": "^5.0.0",
47
48
  "corestore": "^7.9.2",
48
49
  "hyperdrive": "^13.3.1",
49
50
  "hyperswarm": "^4.17.0",
50
51
  "localdrive": "^2.2.1",
51
- "picocolors": "^1.1.1"
52
+ "picocolors": "^1.1.1",
53
+ "simple-git": "^3.33.0"
52
54
  },
53
55
  "devDependencies": {
54
56
  "eslint": "^8.57.0",
@@ -0,0 +1,158 @@
1
+ const simpleGit = require("simple-git");
2
+ const chokidar = require("chokidar");
3
+ const fs = require("fs");
4
+
5
+ class GitSyncService {
6
+ constructor(syncDir, githubRepo, syncMode, debug = false) {
7
+ this.syncDir = syncDir;
8
+ this.githubRepo = githubRepo;
9
+ this.syncMode = syncMode; // 'centralized' or 'decentralized'
10
+ this.debug = debug;
11
+
12
+ // 确保同步目录存在,否则 simple-git 初始化会报错
13
+ if (!fs.existsSync(this.syncDir)) {
14
+ fs.mkdirSync(this.syncDir, { recursive: true });
15
+ }
16
+
17
+ this.git = simpleGit(this.syncDir);
18
+ this.watcher = null;
19
+ this.syncTimeout = null;
20
+ this.isSyncing = false;
21
+ }
22
+
23
+ log(...args) {
24
+ if (this.debug) {
25
+ console.log("[OpenClaw Sync (GitHub)]", ...args);
26
+ }
27
+ }
28
+
29
+ error(...args) {
30
+ console.error("[OpenClaw Sync (GitHub)] ❌", ...args);
31
+ }
32
+
33
+ async init() {
34
+ try {
35
+ const isRepo = await this.git.checkIsRepo();
36
+ if (!isRepo) {
37
+ this.log("初始化本地 Git 仓库...");
38
+ await this.git.init();
39
+ await this.git.addRemote("origin", this.githubRepo);
40
+
41
+ // 尝试拉取远程仓库以防它是非空的
42
+ try {
43
+ await this.git.pull("origin", "main", {
44
+ "--allow-unrelated-histories": null,
45
+ });
46
+ } catch (e) {
47
+ this.log("远程仓库可能为空,或者拉取失败 (可忽略):", e.message);
48
+ }
49
+ } else {
50
+ // 检查并更新 remote
51
+ const remotes = await this.git.getRemotes(true);
52
+ const origin = remotes.find((r) => r.name === "origin");
53
+ if (!origin) {
54
+ await this.git.addRemote("origin", this.githubRepo);
55
+ } else if (origin.refs.fetch !== this.githubRepo) {
56
+ await this.git.removeRemote("origin");
57
+ await this.git.addRemote("origin", this.githubRepo);
58
+ }
59
+
60
+ // 获取当前分支,如果为空则尝试设置为 main
61
+ let currentBranch = "";
62
+ try {
63
+ const branches = await this.git.branch();
64
+ currentBranch = branches.current;
65
+ } catch (e) {
66
+ // ignore
67
+ }
68
+
69
+ if (!currentBranch) {
70
+ try {
71
+ await this.git.checkoutLocalBranch("main");
72
+ } catch (e) {
73
+ // ignore
74
+ }
75
+ }
76
+
77
+ // 启动时自动拉取最新更改
78
+ this.log("拉取远程最新状态...");
79
+ try {
80
+ await this.git.pull("origin", "main");
81
+ this.log("✅ 拉取成功");
82
+ } catch (e) {
83
+ this.error("拉取失败 (可能仓库为空或无 main 分支):", e.message);
84
+ }
85
+ }
86
+
87
+ this.startWatching();
88
+ } catch (err) {
89
+ this.error("初始化失败:", err);
90
+ }
91
+ }
92
+
93
+ startWatching() {
94
+ this.log(`开始监听目录变化: ${this.syncDir}`);
95
+
96
+ // 监听目录变化,忽略 .git 目录
97
+ this.watcher = chokidar.watch(this.syncDir, {
98
+ ignored: /(^|[/\\])\../, // ignore dotfiles
99
+ persistent: true,
100
+ ignoreInitial: true,
101
+ awaitWriteFinish: {
102
+ stabilityThreshold: 2000,
103
+ pollInterval: 100,
104
+ },
105
+ });
106
+
107
+ const triggerSync = (event, filepath) => {
108
+ this.log(`检测到文件变化 [${event}]: ${filepath}`);
109
+
110
+ // 防抖:2秒内没有新变化则触发同步
111
+ if (this.syncTimeout) clearTimeout(this.syncTimeout);
112
+ this.syncTimeout = setTimeout(() => this.performSync(), 2000);
113
+ };
114
+
115
+ this.watcher
116
+ .on("add", (path) => triggerSync("add", path))
117
+ .on("change", (path) => triggerSync("change", path))
118
+ .on("unlink", (path) => triggerSync("unlink", path));
119
+ }
120
+
121
+ async performSync() {
122
+ if (this.isSyncing) return;
123
+ this.isSyncing = true;
124
+
125
+ try {
126
+ const status = await this.git.status();
127
+ if (status.isClean()) {
128
+ this.log("没有需要提交的更改");
129
+ this.isSyncing = false;
130
+ return;
131
+ }
132
+
133
+ this.log("正在提交并推送到 GitHub...");
134
+ await this.git.add("./*");
135
+
136
+ const timestamp = new Date().toISOString();
137
+ await this.git.commit(`Auto-sync: ${timestamp} via OpenClaw`);
138
+
139
+ // 推送
140
+ await this.git.push("origin", "main");
141
+ this.log("✅ 同步推送成功!");
142
+ } catch (err) {
143
+ this.error("同步推送失败:", err);
144
+ } finally {
145
+ this.isSyncing = false;
146
+ }
147
+ }
148
+
149
+ stop() {
150
+ if (this.watcher) {
151
+ this.watcher.close();
152
+ this.log("停止监听文件变化");
153
+ }
154
+ if (this.syncTimeout) clearTimeout(this.syncTimeout);
155
+ }
156
+ }
157
+
158
+ module.exports = GitSyncService;