vibe-config-sync 0.2.3 → 0.3.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 +11 -3
- package/README.zh-CN.md +11 -3
- package/dist/index.js +378 -192
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ When using AI coding tools on multiple machines, configurations like skills, com
|
|
|
21
21
|
| `agents/` | Agent definitions |
|
|
22
22
|
| `plugins/installed_plugins.json` | Plugin registry (machine paths stripped) |
|
|
23
23
|
| `plugins/known_marketplaces.json` | Plugin marketplace sources (machine paths stripped) |
|
|
24
|
+
| `mcp-servers.json` | MCP server configs (extracted from `~/.claude.json`) |
|
|
24
25
|
|
|
25
26
|
**Not synced** (machine-specific / ephemeral / large): `plugins/cache/`, `projects/`, `telemetry/`, `session-env/`, `plans/`, `todos/`, `debug/`, `history.jsonl`, etc.
|
|
26
27
|
|
|
@@ -82,11 +83,11 @@ vibe-sync pull
|
|
|
82
83
|
|
|
83
84
|
## How It Works
|
|
84
85
|
|
|
85
|
-
- **Export** copies config files into `~/.vibe-sync/data/`, stripping machine-specific paths from plugin JSON files. Skills that are symlinks are resolved and their actual contents are copied.
|
|
86
|
+
- **Export** copies config files into `~/.vibe-sync/data/`, stripping machine-specific paths from plugin JSON files. Skills that are symlinks are resolved and their actual contents are copied. MCP server configurations are extracted from `~/.claude.json` and saved as `mcp-servers.json` (prompts for confirmation if `env` fields containing potential secrets are detected).
|
|
86
87
|
|
|
87
|
-
- **Import** restores files from `~/.vibe-sync/data/` to `~/.claude/`, validating JSON structure before overwriting. Plugin JSON files are used as a manifest only (never copied directly) — the `claude` CLI installs plugins and writes correct registry files with local paths. Already-installed plugins are detected and skipped.
|
|
88
|
+
- **Import** restores files from `~/.vibe-sync/data/` to `~/.claude/`, validating JSON structure before overwriting. Plugin JSON files are used as a manifest only (never copied directly) — the `claude` CLI installs plugins and writes correct registry files with local paths. Already-installed plugins are detected and skipped. MCP servers are merged into `~/.claude.json` — only new servers are added; existing servers are never overwritten.
|
|
88
89
|
|
|
89
|
-
- **Backup** is created automatically at `~/.vibe-sync/backups/claude/<timestamp>/` before every import. Use `vibe-sync restore` to list or recover from backups.
|
|
90
|
+
- **Backup** is created automatically at `~/.vibe-sync/backups/claude/<timestamp>/` before every import. Both `~/.claude/` contents and `~/.claude.json` are backed up. Use `vibe-sync restore` to list or recover from backups.
|
|
90
91
|
|
|
91
92
|
## Sync Strategy
|
|
92
93
|
|
|
@@ -118,6 +119,12 @@ In short: new content is added, existing content is overwritten, but nothing is
|
|
|
118
119
|
|
|
119
120
|
Plugin JSON files (`installed_plugins.json`, `known_marketplaces.json`) are exported with machine-specific paths stripped. On import, they are **not** copied to `~/.claude/plugins/` — instead they serve as a manifest. The `claude` CLI is invoked to install each plugin, and it writes correct registry files with local paths as a side effect. Already-installed plugins (detected by `installPath`) are skipped to avoid redundant network operations.
|
|
120
121
|
|
|
122
|
+
### MCP Servers
|
|
123
|
+
|
|
124
|
+
MCP server configurations live in `~/.claude.json` under the `mcpServers` key. On export, this section is extracted and saved as `mcp-servers.json` in the sync directory. If any server config contains `env` fields (which may hold secrets like API keys), the user is prompted before exporting.
|
|
125
|
+
|
|
126
|
+
On import, MCP servers are **additive only** — new servers from the sync repo are added to `~/.claude.json`, but existing servers with the same name are never overwritten. This preserves any machine-specific customizations (e.g., local paths, environment variables).
|
|
127
|
+
|
|
121
128
|
### What This Means in Practice
|
|
122
129
|
|
|
123
130
|
- If both machines modify different configs and then sync, the machine that runs `import` last will lose its local changes (a timestamped backup exists at `~/.vibe-sync/backups/claude/` for manual recovery)
|
|
@@ -129,6 +136,7 @@ Plugin JSON files (`installed_plugins.json`, `known_marketplaces.json`) are expo
|
|
|
129
136
|
| Variable | Description | Default |
|
|
130
137
|
|----------|-------------|---------|
|
|
131
138
|
| `CLAUDE_HOME` | Override Claude config directory | `~/.claude` |
|
|
139
|
+
| `CLAUDE_JSON` | Override Claude global config file (MCP servers) | `~/.claude.json` |
|
|
132
140
|
|
|
133
141
|
## License
|
|
134
142
|
|
package/README.zh-CN.md
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
| `agents/` | 代理定义 |
|
|
22
22
|
| `plugins/installed_plugins.json` | 插件注册表(已剥离机器路径) |
|
|
23
23
|
| `plugins/known_marketplaces.json` | 插件市场来源(已剥离机器路径) |
|
|
24
|
+
| `mcp-servers.json` | MCP 服务器配置(从 `~/.claude.json` 提取) |
|
|
24
25
|
|
|
25
26
|
**不同步**(机器特定 / 临时 / 大文件):`plugins/cache/`、`projects/`、`telemetry/`、`session-env/`、`plans/`、`todos/`、`debug/`、`history.jsonl` 等。
|
|
26
27
|
|
|
@@ -82,11 +83,11 @@ vibe-sync pull
|
|
|
82
83
|
|
|
83
84
|
## 工作原理
|
|
84
85
|
|
|
85
|
-
- **导出**:将配置文件复制到 `~/.vibe-sync/data/`,同时从插件 JSON 文件中剥离机器特定路径。符号链接的技能会被解析并复制其实际内容。
|
|
86
|
+
- **导出**:将配置文件复制到 `~/.vibe-sync/data/`,同时从插件 JSON 文件中剥离机器特定路径。符号链接的技能会被解析并复制其实际内容。MCP 服务器配置从 `~/.claude.json` 中提取并保存为 `mcp-servers.json`(如果检测到包含潜在密钥的 `env` 字段,会提示用户确认)。
|
|
86
87
|
|
|
87
|
-
- **导入**:从 `~/.vibe-sync/data/` 恢复文件到 `~/.claude/`,在覆盖前验证 JSON 结构。插件 JSON 文件仅作为清单使用(不会直接复制)—— `claude` CLI 负责安装插件并写入包含本地路径的正确注册文件。已安装的插件会被检测并跳过。
|
|
88
|
+
- **导入**:从 `~/.vibe-sync/data/` 恢复文件到 `~/.claude/`,在覆盖前验证 JSON 结构。插件 JSON 文件仅作为清单使用(不会直接复制)—— `claude` CLI 负责安装插件并写入包含本地路径的正确注册文件。已安装的插件会被检测并跳过。MCP 服务器以增量方式合并到 `~/.claude.json`——仅添加新服务器,已存在的服务器不会被覆盖。
|
|
88
89
|
|
|
89
|
-
- **备份**:每次导入前自动在 `~/.vibe-sync/backups/claude/<timestamp>/`
|
|
90
|
+
- **备份**:每次导入前自动在 `~/.vibe-sync/backups/claude/<timestamp>/` 创建备份,同时备份 `~/.claude/` 内容和 `~/.claude.json`。使用 `vibe-sync restore` 列出或恢复备份。
|
|
90
91
|
|
|
91
92
|
## 同步策略
|
|
92
93
|
|
|
@@ -118,6 +119,12 @@ vibe-sync pull
|
|
|
118
119
|
|
|
119
120
|
插件 JSON 文件(`installed_plugins.json`、`known_marketplaces.json`)在导出时会剥离机器特定路径。导入时**不会**复制到 `~/.claude/plugins/`——而是作为清单使用。`claude` CLI 被调用来安装每个插件,并在安装过程中写入包含本地路径的正确注册文件。已安装的插件(通过 `installPath` 检测)会被跳过,以避免冗余的网络操作。
|
|
120
121
|
|
|
122
|
+
### MCP 服务器
|
|
123
|
+
|
|
124
|
+
MCP 服务器配置存储在 `~/.claude.json` 的 `mcpServers` 键下。导出时,该部分被提取并保存为同步目录中的 `mcp-servers.json`。如果任何服务器配置包含 `env` 字段(可能包含 API 密钥等密钥),导出前会提示用户确认。
|
|
125
|
+
|
|
126
|
+
导入时,MCP 服务器采用**仅增量**策略——同步仓库中的新服务器会被添加到 `~/.claude.json`,但同名的已有服务器不会被覆盖。这保留了机器特定的自定义配置(如本地路径、环境变量)。
|
|
127
|
+
|
|
121
128
|
### 实际影响
|
|
122
129
|
|
|
123
130
|
- 如果两台机器修改了不同的配置然后同步,最后执行 `import` 的机器会丢失其本地更改(可在 `~/.vibe-sync/backups/claude/` 找到带时间戳的备份进行手动恢复)
|
|
@@ -129,6 +136,7 @@ vibe-sync pull
|
|
|
129
136
|
| 变量 | 说明 | 默认值 |
|
|
130
137
|
|------|------|--------|
|
|
131
138
|
| `CLAUDE_HOME` | 覆盖 Claude 配置目录 | `~/.claude` |
|
|
139
|
+
| `CLAUDE_JSON` | 覆盖 Claude 全局配置文件(MCP 服务器) | `~/.claude.json` |
|
|
132
140
|
|
|
133
141
|
## 许可证
|
|
134
142
|
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import fs from "fs-extra";
|
|
|
8
8
|
import os from "os";
|
|
9
9
|
import path from "path";
|
|
10
10
|
var CLAUDE_HOME = process.env.CLAUDE_HOME ?? path.join(os.homedir(), ".claude");
|
|
11
|
+
var CLAUDE_JSON = process.env.CLAUDE_JSON ?? path.join(os.homedir(), ".claude.json");
|
|
11
12
|
var SYNC_DIR = path.join(os.homedir(), ".vibe-sync");
|
|
12
13
|
var BACKUP_BASE = path.join(SYNC_DIR, "backups", "claude");
|
|
13
14
|
function getConfigDir() {
|
|
@@ -19,10 +20,12 @@ function isInitialized() {
|
|
|
19
20
|
var SYNC_FILES = ["settings.json", "CLAUDE.md"];
|
|
20
21
|
var SYNC_DIRS = ["commands", "agents"];
|
|
21
22
|
var PLUGIN_FILES = ["installed_plugins.json", "known_marketplaces.json"];
|
|
23
|
+
var MCP_SYNC_FILE = "mcp-servers.json";
|
|
22
24
|
|
|
23
25
|
// src/commands/export.ts
|
|
24
26
|
import fs4 from "fs-extra";
|
|
25
27
|
import path4 from "path";
|
|
28
|
+
import readline from "readline/promises";
|
|
26
29
|
|
|
27
30
|
// src/core/logger.ts
|
|
28
31
|
import pc from "picocolors";
|
|
@@ -64,8 +67,11 @@ function readJsonSafe(filePath) {
|
|
|
64
67
|
return fs2.readJsonSync(filePath);
|
|
65
68
|
} catch (err) {
|
|
66
69
|
const code = err.code;
|
|
67
|
-
if (code
|
|
68
|
-
|
|
70
|
+
if (code === "ENOENT") return null;
|
|
71
|
+
if (err instanceof SyntaxError) {
|
|
72
|
+
logWarn(`Failed to parse JSON ${filePath}: ${err.message}`);
|
|
73
|
+
} else {
|
|
74
|
+
logWarn(`Failed to read ${filePath}: ${err.message}`);
|
|
69
75
|
}
|
|
70
76
|
return null;
|
|
71
77
|
}
|
|
@@ -76,6 +82,11 @@ function writeJsonSafe(filePath, data) {
|
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
// src/core/sanitize.ts
|
|
85
|
+
function mcpServersHaveEnv(servers) {
|
|
86
|
+
return Object.values(servers).some(
|
|
87
|
+
(config) => typeof config === "object" && config !== null && "env" in config
|
|
88
|
+
);
|
|
89
|
+
}
|
|
79
90
|
function sanitizePlugins(data) {
|
|
80
91
|
const result = structuredClone(data);
|
|
81
92
|
for (const entries of Object.values(result.plugins ?? {})) {
|
|
@@ -134,14 +145,34 @@ function importSkills(srcDir, destDir) {
|
|
|
134
145
|
if (entry.isDirectory()) {
|
|
135
146
|
const src = path3.join(srcDir, entry.name);
|
|
136
147
|
const dest = path3.join(destDir, entry.name);
|
|
137
|
-
|
|
148
|
+
copyDirClean(src, dest);
|
|
138
149
|
logInfo(`Imported skill: ${entry.name}`);
|
|
139
150
|
}
|
|
140
151
|
}
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
// src/commands/export.ts
|
|
144
|
-
function
|
|
155
|
+
async function confirmMcpExport() {
|
|
156
|
+
const rl = readline.createInterface({
|
|
157
|
+
input: process.stdin,
|
|
158
|
+
output: process.stdout
|
|
159
|
+
});
|
|
160
|
+
try {
|
|
161
|
+
const answer = await rl.question(
|
|
162
|
+
'[WARN] MCP server configs contain "env" fields that may include secrets (API keys, tokens).\n Export anyway? (y/N): '
|
|
163
|
+
);
|
|
164
|
+
return answer.trim().toLowerCase() === "y";
|
|
165
|
+
} finally {
|
|
166
|
+
rl.close();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function cmdExport() {
|
|
170
|
+
if (!fs4.existsSync(CLAUDE_HOME)) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Claude config directory not found: ${CLAUDE_HOME}
|
|
173
|
+
Make sure Claude Code has been run at least once.`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
145
176
|
const configDir = getConfigDir();
|
|
146
177
|
const pluginsDir = path4.join(configDir, "plugins");
|
|
147
178
|
fs4.ensureDirSync(pluginsDir);
|
|
@@ -187,6 +218,21 @@ function cmdExport() {
|
|
|
187
218
|
);
|
|
188
219
|
logInfo("Exported: plugins/known_marketplaces.json (sanitized)");
|
|
189
220
|
}
|
|
221
|
+
const claudeJson = readJsonSafe(CLAUDE_JSON);
|
|
222
|
+
const mcpServers = claudeJson?.mcpServers;
|
|
223
|
+
if (typeof mcpServers === "object" && mcpServers !== null) {
|
|
224
|
+
const servers = mcpServers;
|
|
225
|
+
let shouldExport = true;
|
|
226
|
+
if (mcpServersHaveEnv(servers)) {
|
|
227
|
+
shouldExport = await confirmMcpExport();
|
|
228
|
+
}
|
|
229
|
+
if (shouldExport) {
|
|
230
|
+
writeJsonSafe(path4.join(configDir, MCP_SYNC_FILE), servers);
|
|
231
|
+
logInfo(`Exported: ${MCP_SYNC_FILE} (from ~/.claude.json)`);
|
|
232
|
+
} else {
|
|
233
|
+
logWarn(`Skipped: ${MCP_SYNC_FILE} (user declined due to env secrets)`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
190
236
|
logOk("Export complete");
|
|
191
237
|
}
|
|
192
238
|
|
|
@@ -218,22 +264,24 @@ function backupExisting() {
|
|
|
218
264
|
fs5.copySync(skillsDir, path5.join(backupDir, "skills"));
|
|
219
265
|
}
|
|
220
266
|
const pluginsDir = path5.join(CLAUDE_HOME, "plugins");
|
|
267
|
+
fs5.ensureDirSync(path5.join(backupDir, "plugins"));
|
|
221
268
|
for (const file of PLUGIN_FILES) {
|
|
222
269
|
const src = path5.join(pluginsDir, file);
|
|
223
270
|
if (fs5.existsSync(src)) {
|
|
224
|
-
fs5.ensureDirSync(path5.join(backupDir, "plugins"));
|
|
225
271
|
fs5.copySync(src, path5.join(backupDir, "plugins", file));
|
|
226
272
|
}
|
|
227
273
|
}
|
|
274
|
+
if (fs5.existsSync(CLAUDE_JSON)) {
|
|
275
|
+
fs5.copySync(CLAUDE_JSON, path5.join(backupDir, ".claude.json"));
|
|
276
|
+
}
|
|
228
277
|
logOk(`Backup created: ${backupDir}`);
|
|
229
|
-
return backupDir;
|
|
230
278
|
}
|
|
231
279
|
function listBackups() {
|
|
232
280
|
if (!fs5.existsSync(BACKUP_BASE)) return [];
|
|
233
281
|
return fs5.readdirSync(BACKUP_BASE, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort().reverse();
|
|
234
282
|
}
|
|
235
283
|
function restoreFromBackup(backupName) {
|
|
236
|
-
if (!backupName || backupName.includes("/") || backupName.includes("\\")
|
|
284
|
+
if (!backupName || backupName.includes("/") || backupName.includes("\\")) {
|
|
237
285
|
throw new Error(`Invalid backup name: ${backupName}`);
|
|
238
286
|
}
|
|
239
287
|
const backupDir = path5.join(BACKUP_BASE, backupName);
|
|
@@ -275,11 +323,16 @@ function restoreFromBackup(backupName) {
|
|
|
275
323
|
}
|
|
276
324
|
}
|
|
277
325
|
}
|
|
326
|
+
const claudeJsonSrc = path5.join(backupDir, ".claude.json");
|
|
327
|
+
if (fs5.existsSync(claudeJsonSrc)) {
|
|
328
|
+
fs5.copySync(claudeJsonSrc, CLAUDE_JSON);
|
|
329
|
+
logInfo("Restored: .claude.json");
|
|
330
|
+
}
|
|
278
331
|
logOk(`Restored from backup: ${backupName}`);
|
|
279
332
|
}
|
|
280
333
|
|
|
281
334
|
// src/core/plugins.ts
|
|
282
|
-
import {
|
|
335
|
+
import { spawnSync } from "child_process";
|
|
283
336
|
function isNonNullObject(value) {
|
|
284
337
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
285
338
|
}
|
|
@@ -299,17 +352,12 @@ function execClaude(args) {
|
|
|
299
352
|
return result.status === 0;
|
|
300
353
|
}
|
|
301
354
|
function isClaudeAvailable() {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return true;
|
|
305
|
-
} catch {
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
355
|
+
const result = spawnSync("claude", ["--version"], { stdio: "pipe", timeout: 5e3, shell: true });
|
|
356
|
+
return result.status === 0;
|
|
308
357
|
}
|
|
309
358
|
function reinstallPlugins(marketplacesFile, pluginsFile, settingsFile, existingState) {
|
|
310
359
|
if (!isClaudeAvailable()) {
|
|
311
|
-
|
|
312
|
-
return;
|
|
360
|
+
throw new Error("claude CLI not found. Cannot reinstall plugins.");
|
|
313
361
|
}
|
|
314
362
|
logInfo("Phase 1: Adding plugin marketplaces...");
|
|
315
363
|
const marketplaces = readJsonSafe(marketplacesFile);
|
|
@@ -417,24 +465,19 @@ function validatePluginsJson(filePath) {
|
|
|
417
465
|
}
|
|
418
466
|
return ok(data);
|
|
419
467
|
}
|
|
420
|
-
function validateMarketplacesJson(filePath) {
|
|
421
|
-
return validateJsonFile(filePath);
|
|
422
|
-
}
|
|
423
468
|
|
|
424
469
|
// src/commands/import.ts
|
|
425
|
-
function
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (dryRun) logInfo("Dry run - no changes will be made\n");
|
|
435
|
-
if (!dryRun) backupExisting();
|
|
470
|
+
function validateSyncFile(file, src) {
|
|
471
|
+
if (file === "settings.json") return validateSettingsJson(src);
|
|
472
|
+
return validateJsonFile(src);
|
|
473
|
+
}
|
|
474
|
+
function validatePluginFile(file, src) {
|
|
475
|
+
if (file === "known_marketplaces.json") return validateJsonFile(src);
|
|
476
|
+
return validatePluginsJson(src);
|
|
477
|
+
}
|
|
478
|
+
function captureExistingPluginState() {
|
|
436
479
|
const pluginsDest = path6.join(CLAUDE_HOME, "plugins");
|
|
437
|
-
|
|
480
|
+
return {
|
|
438
481
|
marketplaces: readJsonSafe(
|
|
439
482
|
path6.join(pluginsDest, "known_marketplaces.json")
|
|
440
483
|
),
|
|
@@ -445,39 +488,48 @@ Run "vibe-sync export" first or "vibe-sync pull"`
|
|
|
445
488
|
path6.join(CLAUDE_HOME, "settings.json")
|
|
446
489
|
)
|
|
447
490
|
};
|
|
491
|
+
}
|
|
492
|
+
function importFile(file, src, stripPluginSettings, dryRun) {
|
|
493
|
+
const dest = path6.join(CLAUDE_HOME, file);
|
|
494
|
+
const exists = fs6.existsSync(dest);
|
|
495
|
+
if (dryRun) {
|
|
496
|
+
const suffix2 = stripPluginSettings ? " (without enabledPlugins)" : "";
|
|
497
|
+
logInfo(`Would import: ${file}${exists ? " (overwrite)" : " (new)"}${suffix2}`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
fs6.ensureDirSync(path6.dirname(dest));
|
|
501
|
+
if (stripPluginSettings) {
|
|
502
|
+
const data = readJsonSafe(src);
|
|
503
|
+
if (!data) {
|
|
504
|
+
logWarn(`Skipping ${file}: invalid JSON, cannot strip enabledPlugins`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
delete data.enabledPlugins;
|
|
508
|
+
fs6.writeJsonSync(dest, data, { spaces: 2 });
|
|
509
|
+
} else {
|
|
510
|
+
fs6.copySync(src, dest);
|
|
511
|
+
}
|
|
512
|
+
const suffix = stripPluginSettings ? " (stripped enabledPlugins)" : "";
|
|
513
|
+
logInfo(`Imported: ${file}${suffix}`);
|
|
514
|
+
}
|
|
515
|
+
function importSyncFiles(configDir, options) {
|
|
516
|
+
const dryRun = !!options.dryRun;
|
|
448
517
|
for (const file of SYNC_FILES) {
|
|
449
518
|
const src = path6.join(configDir, file);
|
|
450
519
|
if (!fs6.existsSync(src)) continue;
|
|
451
520
|
if (file.endsWith(".json")) {
|
|
452
|
-
const result = file
|
|
521
|
+
const result = validateSyncFile(file, src);
|
|
453
522
|
if (!result.valid) {
|
|
454
|
-
|
|
455
|
-
else logWarn(`Skipping ${file}: ${result.errors.join(", ")}`);
|
|
523
|
+
logWarn(dryRun ? ` INVALID: ${result.errors.join(", ")}` : `Skipping ${file}: ${result.errors.join(", ")}`);
|
|
456
524
|
continue;
|
|
457
525
|
}
|
|
458
526
|
if (dryRun) logOk(` Validated: ${file}`);
|
|
459
527
|
}
|
|
460
|
-
const dest = path6.join(CLAUDE_HOME, file);
|
|
461
|
-
const exists = fs6.existsSync(dest);
|
|
462
528
|
const stripPluginSettings = file === "settings.json" && !options.reinstallPlugins;
|
|
463
|
-
|
|
464
|
-
logInfo(`Would import: ${file}${exists ? " (overwrite)" : " (new)"}${stripPluginSettings ? " (without enabledPlugins)" : ""}`);
|
|
465
|
-
} else {
|
|
466
|
-
fs6.ensureDirSync(path6.dirname(dest));
|
|
467
|
-
if (stripPluginSettings) {
|
|
468
|
-
const data = readJsonSafe(src);
|
|
469
|
-
if (data) {
|
|
470
|
-
delete data.enabledPlugins;
|
|
471
|
-
fs6.writeJsonSync(dest, data, { spaces: 2 });
|
|
472
|
-
} else {
|
|
473
|
-
fs6.copySync(src, dest);
|
|
474
|
-
}
|
|
475
|
-
} else {
|
|
476
|
-
fs6.copySync(src, dest);
|
|
477
|
-
}
|
|
478
|
-
logInfo(`Imported: ${file}${stripPluginSettings ? " (stripped enabledPlugins)" : ""}`);
|
|
479
|
-
}
|
|
529
|
+
importFile(file, src, stripPluginSettings, dryRun);
|
|
480
530
|
}
|
|
531
|
+
}
|
|
532
|
+
function importSyncDirs(configDir, dryRun) {
|
|
481
533
|
for (const dir of SYNC_DIRS) {
|
|
482
534
|
const src = path6.join(configDir, dir);
|
|
483
535
|
if (!fs6.existsSync(src)) continue;
|
|
@@ -489,6 +541,8 @@ Run "vibe-sync export" first or "vibe-sync pull"`
|
|
|
489
541
|
logInfo(`Imported: ${dir}/`);
|
|
490
542
|
}
|
|
491
543
|
}
|
|
544
|
+
}
|
|
545
|
+
function importSyncSkills(configDir, dryRun) {
|
|
492
546
|
const skillsSrc = path6.join(configDir, "skills");
|
|
493
547
|
if (dryRun && fs6.existsSync(skillsSrc)) {
|
|
494
548
|
const entries = fs6.readdirSync(skillsSrc, { withFileTypes: true });
|
|
@@ -498,50 +552,113 @@ Run "vibe-sync export" first or "vibe-sync pull"`
|
|
|
498
552
|
} else if (!dryRun) {
|
|
499
553
|
importSkills(skillsSrc, path6.join(CLAUDE_HOME, "skills"));
|
|
500
554
|
}
|
|
555
|
+
}
|
|
556
|
+
function readLocalMcpServers() {
|
|
557
|
+
const json = readJsonSafe(CLAUDE_JSON) ?? {};
|
|
558
|
+
const mcpServers = json.mcpServers;
|
|
559
|
+
const servers = typeof mcpServers === "object" && mcpServers !== null ? mcpServers : {};
|
|
560
|
+
return { json, servers };
|
|
561
|
+
}
|
|
562
|
+
function importMcpServers(configDir, dryRun) {
|
|
563
|
+
const src = path6.join(configDir, MCP_SYNC_FILE);
|
|
564
|
+
if (!fs6.existsSync(src)) return;
|
|
565
|
+
const result = validateJsonFile(src);
|
|
566
|
+
if (!result.valid) {
|
|
567
|
+
logWarn(dryRun ? ` INVALID: ${result.errors.join(", ")}` : `Skipping ${MCP_SYNC_FILE}: ${result.errors.join(", ")}`);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (dryRun) logOk(` Validated: ${MCP_SYNC_FILE}`);
|
|
571
|
+
const syncedServers = readJsonSafe(src);
|
|
572
|
+
if (!syncedServers) return;
|
|
573
|
+
const { json: existing, servers: currentServers } = readLocalMcpServers();
|
|
574
|
+
const syncedNames = Object.keys(syncedServers);
|
|
575
|
+
const newNames = syncedNames.filter((n) => !(n in currentServers));
|
|
576
|
+
if (dryRun) {
|
|
577
|
+
const skipNames = syncedNames.filter((n) => n in currentServers);
|
|
578
|
+
if (newNames.length > 0) logInfo(`Would import MCP servers: ${newNames.join(", ")}`);
|
|
579
|
+
if (skipNames.length > 0) logWarn(`Would skip (already exist): ${skipNames.join(", ")}`);
|
|
580
|
+
if (newNames.length === 0) logInfo("No new MCP servers to import");
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (newNames.length === 0) {
|
|
584
|
+
logInfo("No new MCP servers to import (all already exist locally)");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
for (const name of syncedNames) {
|
|
588
|
+
if (name in currentServers) {
|
|
589
|
+
logWarn(`MCP server already exists locally, skipping: ${name}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const newServers = Object.fromEntries(
|
|
593
|
+
newNames.map((name) => [name, syncedServers[name]])
|
|
594
|
+
);
|
|
595
|
+
existing.mcpServers = { ...currentServers, ...newServers };
|
|
596
|
+
fs6.writeJsonSync(CLAUDE_JSON, existing, { spaces: 2 });
|
|
597
|
+
logInfo(`Imported: ${MCP_SYNC_FILE} \u2192 ~/.claude.json (added ${newNames.length} new server(s))`);
|
|
598
|
+
}
|
|
599
|
+
function syncPlugins(configDir, existingState, dryRun) {
|
|
501
600
|
const pluginsSrc = path6.join(configDir, "plugins");
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
logWarn(`${dryRun ? " INVALID" : `Invalid plugins/${file}`}: ${result.errors.join(", ")}`);
|
|
520
|
-
validationPassed = false;
|
|
521
|
-
} else if (dryRun) {
|
|
522
|
-
logOk(` Validated: plugins/${file}`);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (!validationPassed) {
|
|
526
|
-
logWarn("Skipping plugin reinstallation due to validation errors");
|
|
527
|
-
} else if (dryRun) {
|
|
528
|
-
logInfo("Would reinstall plugins via claude CLI");
|
|
529
|
-
} else {
|
|
530
|
-
reinstallPlugins(
|
|
531
|
-
path6.join(pluginsSrc, "known_marketplaces.json"),
|
|
532
|
-
path6.join(pluginsSrc, "installed_plugins.json"),
|
|
533
|
-
path6.join(CLAUDE_HOME, "settings.json"),
|
|
534
|
-
existingState
|
|
535
|
-
);
|
|
536
|
-
}
|
|
601
|
+
const hasManifest = PLUGIN_FILES.some(
|
|
602
|
+
(f) => fs6.existsSync(path6.join(pluginsSrc, f))
|
|
603
|
+
);
|
|
604
|
+
if (!hasManifest) {
|
|
605
|
+
logInfo("No plugin manifest files found in sync directory");
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
let validationPassed = true;
|
|
609
|
+
for (const file of PLUGIN_FILES) {
|
|
610
|
+
const src = path6.join(pluginsSrc, file);
|
|
611
|
+
if (!fs6.existsSync(src)) continue;
|
|
612
|
+
const result = validatePluginFile(file, src);
|
|
613
|
+
if (!result.valid) {
|
|
614
|
+
logWarn(dryRun ? ` INVALID: ${result.errors.join(", ")}` : `Invalid plugins/${file}: ${result.errors.join(", ")}`);
|
|
615
|
+
validationPassed = false;
|
|
616
|
+
} else if (dryRun) {
|
|
617
|
+
logOk(` Validated: plugins/${file}`);
|
|
537
618
|
}
|
|
619
|
+
}
|
|
620
|
+
if (!validationPassed) {
|
|
621
|
+
logWarn("Skipping plugin reinstallation due to validation errors");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (dryRun) {
|
|
625
|
+
logInfo("Would reinstall plugins via claude CLI");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
reinstallPlugins(
|
|
629
|
+
path6.join(pluginsSrc, "known_marketplaces.json"),
|
|
630
|
+
path6.join(pluginsSrc, "installed_plugins.json"),
|
|
631
|
+
path6.join(CLAUDE_HOME, "settings.json"),
|
|
632
|
+
existingState
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
function cmdImport(options = {}) {
|
|
636
|
+
const configDir = getConfigDir();
|
|
637
|
+
if (!fs6.existsSync(configDir)) {
|
|
638
|
+
throw new Error(
|
|
639
|
+
`Config directory not found: ${configDir}
|
|
640
|
+
Run "vibe-sync export" first or "vibe-sync pull"`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
const dryRun = !!options.dryRun;
|
|
644
|
+
if (dryRun) logInfo("Dry run - no changes will be made\n");
|
|
645
|
+
if (!dryRun) backupExisting();
|
|
646
|
+
const existingState = captureExistingPluginState();
|
|
647
|
+
importSyncFiles(configDir, options);
|
|
648
|
+
importSyncDirs(configDir, dryRun);
|
|
649
|
+
importSyncSkills(configDir, dryRun);
|
|
650
|
+
importMcpServers(configDir, dryRun);
|
|
651
|
+
if (options.reinstallPlugins) {
|
|
652
|
+
syncPlugins(configDir, existingState, dryRun);
|
|
538
653
|
} else {
|
|
539
|
-
logInfo("Skipping plugin sync (
|
|
654
|
+
logInfo("Skipping plugin sync (remove --no-plugins flag to enable)");
|
|
540
655
|
}
|
|
541
656
|
if (!dryRun) logOk("Import complete");
|
|
542
657
|
}
|
|
543
658
|
|
|
544
659
|
// src/commands/status.ts
|
|
660
|
+
import fs8 from "fs-extra";
|
|
661
|
+
import os2 from "os";
|
|
545
662
|
import path8 from "path";
|
|
546
663
|
|
|
547
664
|
// src/core/diff.ts
|
|
@@ -552,9 +669,6 @@ import pc2 from "picocolors";
|
|
|
552
669
|
function readNormalized(filePath) {
|
|
553
670
|
return fs7.readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n");
|
|
554
671
|
}
|
|
555
|
-
function filesEqual(a, b) {
|
|
556
|
-
return readNormalized(a) === readNormalized(b);
|
|
557
|
-
}
|
|
558
672
|
function dirsEqual(a, b) {
|
|
559
673
|
const aEntries = fs7.readdirSync(a).sort();
|
|
560
674
|
const bEntries = fs7.readdirSync(b).sort();
|
|
@@ -563,36 +677,37 @@ function dirsEqual(a, b) {
|
|
|
563
677
|
if (aEntries[i] !== bEntries[i]) return false;
|
|
564
678
|
const aPath = path7.join(a, aEntries[i]);
|
|
565
679
|
const bPath = path7.join(b, bEntries[i]);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
680
|
+
try {
|
|
681
|
+
const aStat = fs7.statSync(aPath);
|
|
682
|
+
const bStat = fs7.statSync(bPath);
|
|
683
|
+
if (aStat.isDirectory() !== bStat.isDirectory()) return false;
|
|
684
|
+
const equal = aStat.isDirectory() ? dirsEqual(aPath, bPath) : readNormalized(aPath) === readNormalized(bPath);
|
|
685
|
+
if (!equal) return false;
|
|
686
|
+
} catch {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
571
689
|
}
|
|
572
690
|
return true;
|
|
573
691
|
}
|
|
574
|
-
function
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
return line;
|
|
580
|
-
}).join("\n");
|
|
692
|
+
function colorDiffLine(line) {
|
|
693
|
+
if (line.startsWith("@@")) return pc2.cyan(line);
|
|
694
|
+
if (line.startsWith("+")) return pc2.green(line);
|
|
695
|
+
if (line.startsWith("-")) return pc2.red(line);
|
|
696
|
+
return line;
|
|
581
697
|
}
|
|
582
698
|
function showDiff(localPath, repoPath, label) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (!
|
|
699
|
+
const localExists = fs7.existsSync(localPath);
|
|
700
|
+
const repoExists = fs7.existsSync(repoPath);
|
|
701
|
+
if (!localExists && !repoExists) return false;
|
|
702
|
+
if (!localExists) {
|
|
587
703
|
console.log(pc2.yellow(` ${label}: exists in repo but not locally`));
|
|
588
704
|
return true;
|
|
589
705
|
}
|
|
590
|
-
if (!
|
|
706
|
+
if (!repoExists) {
|
|
591
707
|
console.log(pc2.yellow(` ${label}: exists locally but not in repo`));
|
|
592
708
|
return true;
|
|
593
709
|
}
|
|
594
|
-
|
|
595
|
-
if (isDir) {
|
|
710
|
+
if (fs7.statSync(localPath).isDirectory()) {
|
|
596
711
|
if (dirsEqual(localPath, repoPath)) return false;
|
|
597
712
|
console.log(pc2.yellow(` ${label}: differs`));
|
|
598
713
|
return true;
|
|
@@ -607,7 +722,7 @@ function showDiff(localPath, repoPath, label) {
|
|
|
607
722
|
localContent,
|
|
608
723
|
repoContent
|
|
609
724
|
);
|
|
610
|
-
console.log(
|
|
725
|
+
console.log(patch.split("\n").map(colorDiffLine).join("\n"));
|
|
611
726
|
return true;
|
|
612
727
|
}
|
|
613
728
|
|
|
@@ -634,13 +749,30 @@ function cmdStatus() {
|
|
|
634
749
|
if (showDiff(localSkills, repoSkills, "skills/")) {
|
|
635
750
|
hasDiff = true;
|
|
636
751
|
}
|
|
637
|
-
for (const file of
|
|
752
|
+
for (const file of PLUGIN_FILES) {
|
|
638
753
|
const local = path8.join(CLAUDE_HOME, "plugins", file);
|
|
639
754
|
const repo = path8.join(configDir, "plugins", file);
|
|
640
755
|
if (showDiff(local, repo, `plugins/${file}`)) {
|
|
641
756
|
hasDiff = true;
|
|
642
757
|
}
|
|
643
758
|
}
|
|
759
|
+
const repoMcp = path8.join(configDir, MCP_SYNC_FILE);
|
|
760
|
+
const claudeJson = readJsonSafe(CLAUDE_JSON);
|
|
761
|
+
const localMcp = typeof claudeJson?.mcpServers === "object" && claudeJson.mcpServers !== null ? claudeJson.mcpServers : void 0;
|
|
762
|
+
if (localMcp || fs8.existsSync(repoMcp)) {
|
|
763
|
+
const tmpFile = path8.join(os2.tmpdir(), `vibe-sync-mcp-${Date.now()}.json`);
|
|
764
|
+
try {
|
|
765
|
+
if (localMcp) {
|
|
766
|
+
fs8.writeJsonSync(tmpFile, localMcp, { spaces: 2 });
|
|
767
|
+
}
|
|
768
|
+
const localPath = localMcp ? tmpFile : "";
|
|
769
|
+
if (showDiff(localPath, repoMcp, MCP_SYNC_FILE)) {
|
|
770
|
+
hasDiff = true;
|
|
771
|
+
}
|
|
772
|
+
} finally {
|
|
773
|
+
fs8.removeSync(tmpFile);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
644
776
|
if (!hasDiff) {
|
|
645
777
|
logOk("No differences found");
|
|
646
778
|
}
|
|
@@ -666,43 +798,85 @@ async function hasRemote(git) {
|
|
|
666
798
|
return false;
|
|
667
799
|
}
|
|
668
800
|
}
|
|
801
|
+
function setUpstreamArgs(remoteBranch, localBranch) {
|
|
802
|
+
return ["--set-upstream-to=origin/" + remoteBranch, localBranch];
|
|
803
|
+
}
|
|
669
804
|
async function commitAndPush(git) {
|
|
670
805
|
await ensureGitRepo(git);
|
|
671
806
|
await git.add("-A");
|
|
672
807
|
const status = await git.status();
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
} else {
|
|
808
|
+
const hasChanges = !status.isClean();
|
|
809
|
+
if (hasChanges) {
|
|
676
810
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", "_");
|
|
677
811
|
const msg = `sync: update claude configs ${timestamp}`;
|
|
678
812
|
await git.commit(msg);
|
|
679
813
|
logOk(`Committed: ${msg}`);
|
|
680
|
-
}
|
|
681
|
-
if (await hasRemote(git)) {
|
|
682
|
-
const branch = (await git.branchLocal()).current || "main";
|
|
683
|
-
await git.push(["-u", "origin", branch]);
|
|
684
|
-
logOk("Pushed to remote");
|
|
685
814
|
} else {
|
|
815
|
+
logInfo("No changes to commit");
|
|
816
|
+
}
|
|
817
|
+
if (!await hasRemote(git)) {
|
|
686
818
|
logWarn("No remote configured. Run: git remote add origin <url>");
|
|
819
|
+
return hasChanges;
|
|
820
|
+
}
|
|
821
|
+
const branch = (await git.branchLocal()).current || "main";
|
|
822
|
+
try {
|
|
823
|
+
await git.push(["-u", "origin", branch]);
|
|
824
|
+
} catch {
|
|
825
|
+
logInfo("Push rejected, pulling remote changes first");
|
|
826
|
+
await pullFromRemote(git);
|
|
827
|
+
await git.push(["-u", "origin", branch]);
|
|
828
|
+
}
|
|
829
|
+
logOk("Pushed to remote");
|
|
830
|
+
return hasChanges;
|
|
831
|
+
}
|
|
832
|
+
async function detectRemoteBranch(git) {
|
|
833
|
+
try {
|
|
834
|
+
const remote = await git.remote(["show", "origin"]);
|
|
835
|
+
if (remote) {
|
|
836
|
+
const match = remote.match(/HEAD branch:\s*(\S+)/);
|
|
837
|
+
if (match) return match[1];
|
|
838
|
+
}
|
|
839
|
+
} catch {
|
|
687
840
|
}
|
|
688
|
-
|
|
841
|
+
try {
|
|
842
|
+
const refs = await git.listRemote(["--heads", "origin"]);
|
|
843
|
+
if (refs.includes("refs/heads/main")) return "main";
|
|
844
|
+
if (refs.includes("refs/heads/master")) return "master";
|
|
845
|
+
} catch {
|
|
846
|
+
}
|
|
847
|
+
return "main";
|
|
689
848
|
}
|
|
690
849
|
async function pullFromRemote(git) {
|
|
691
850
|
if (!await hasRemote(git)) {
|
|
692
851
|
throw new Error("No remote configured. Run: git remote add origin <url>");
|
|
693
852
|
}
|
|
694
|
-
const
|
|
853
|
+
const localBranch = (await git.branchLocal()).current;
|
|
854
|
+
const remoteBranch = await detectRemoteBranch(git);
|
|
855
|
+
const branch = localBranch || remoteBranch;
|
|
695
856
|
try {
|
|
696
857
|
await git.pull();
|
|
697
858
|
} catch {
|
|
698
859
|
try {
|
|
699
|
-
await git.pull("origin",
|
|
700
|
-
await git.branch(
|
|
860
|
+
await git.pull("origin", remoteBranch);
|
|
861
|
+
await git.branch(setUpstreamArgs(remoteBranch, branch));
|
|
862
|
+
} catch {
|
|
863
|
+
logWarn("Merge conflict detected. Stashing local changes before resetting to remote version.");
|
|
864
|
+
await git.fetch("origin", remoteBranch);
|
|
865
|
+
try {
|
|
866
|
+
await git.stash(["push", "-m", `vibe-sync: pre-reset backup ${(/* @__PURE__ */ new Date()).toISOString()}`]);
|
|
867
|
+
logInfo('Local changes stashed. Use "git stash pop" in ~/.vibe-sync to recover if needed.');
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
await git.reset(["--hard", `origin/${remoteBranch}`]);
|
|
871
|
+
await git.clean("f", ["-d"]);
|
|
872
|
+
await git.branch(setUpstreamArgs(remoteBranch, branch));
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (localBranch && localBranch !== remoteBranch) {
|
|
876
|
+
try {
|
|
877
|
+
await git.branch(["-m", localBranch, remoteBranch]);
|
|
878
|
+
await git.branch(setUpstreamArgs(remoteBranch, remoteBranch));
|
|
701
879
|
} catch {
|
|
702
|
-
logWarn("Merge conflict detected, resetting to remote version");
|
|
703
|
-
await git.fetch("origin", branch);
|
|
704
|
-
await git.reset(["--hard", `origin/${branch}`]);
|
|
705
|
-
await git.branch(["--set-upstream-to=origin/" + branch, branch]);
|
|
706
880
|
}
|
|
707
881
|
}
|
|
708
882
|
logOk("Pulled from remote");
|
|
@@ -710,7 +884,7 @@ async function pullFromRemote(git) {
|
|
|
710
884
|
|
|
711
885
|
// src/commands/push.ts
|
|
712
886
|
async function cmdPush() {
|
|
713
|
-
cmdExport();
|
|
887
|
+
await cmdExport();
|
|
714
888
|
const git = createGit(SYNC_DIR);
|
|
715
889
|
await commitAndPush(git);
|
|
716
890
|
}
|
|
@@ -719,7 +893,24 @@ async function cmdPush() {
|
|
|
719
893
|
async function cmdPull(options = {}) {
|
|
720
894
|
const git = createGit(SYNC_DIR);
|
|
721
895
|
if (options.dryRun) {
|
|
722
|
-
|
|
896
|
+
if (await hasRemote(git)) {
|
|
897
|
+
await git.fetch();
|
|
898
|
+
const localHead = await git.revparse(["HEAD"]);
|
|
899
|
+
const remoteHead = await git.revparse(["FETCH_HEAD"]).catch(() => null);
|
|
900
|
+
if (remoteHead && localHead !== remoteHead) {
|
|
901
|
+
const summary = await git.diffSummary(["HEAD", "FETCH_HEAD"]);
|
|
902
|
+
logInfo(`Remote has ${summary.changed} changed file(s) ahead of local:`);
|
|
903
|
+
for (const file of summary.files) {
|
|
904
|
+
const ins = "insertions" in file ? file.insertions : 0;
|
|
905
|
+
const del = "deletions" in file ? file.deletions : 0;
|
|
906
|
+
logInfo(` ${file.file} (+${ins} -${del})`);
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
logInfo("Remote is up to date with local");
|
|
910
|
+
}
|
|
911
|
+
} else {
|
|
912
|
+
logWarn("No remote configured, cannot fetch for preview");
|
|
913
|
+
}
|
|
723
914
|
} else {
|
|
724
915
|
await pullFromRemote(git);
|
|
725
916
|
}
|
|
@@ -727,87 +918,77 @@ async function cmdPull(options = {}) {
|
|
|
727
918
|
}
|
|
728
919
|
|
|
729
920
|
// src/commands/init.ts
|
|
730
|
-
import
|
|
921
|
+
import fs9 from "fs-extra";
|
|
731
922
|
import path9 from "path";
|
|
732
|
-
import
|
|
923
|
+
import readline2 from "readline/promises";
|
|
924
|
+
var GIT_URL_PATTERN = /^(https?:\/\/|git@|ssh:\/\/|git:\/\/).+/;
|
|
733
925
|
function createReadline() {
|
|
734
|
-
return
|
|
926
|
+
return readline2.createInterface({
|
|
735
927
|
input: process.stdin,
|
|
736
928
|
output: process.stdout
|
|
737
929
|
});
|
|
738
930
|
}
|
|
739
|
-
async function
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
931
|
+
async function promptUrl(rl, prompt) {
|
|
932
|
+
const url = await rl.question(prompt);
|
|
933
|
+
const trimmed = url.trim();
|
|
934
|
+
if (!trimmed) return null;
|
|
935
|
+
if (!GIT_URL_PATTERN.test(trimmed)) {
|
|
936
|
+
logError("Invalid Git URL format. Expected https://, git@, ssh://, or git:// URL");
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
return trimmed;
|
|
940
|
+
}
|
|
941
|
+
async function updateRemote() {
|
|
942
|
+
const git = createGit(SYNC_DIR);
|
|
943
|
+
const remotes = await git.getRemotes(true);
|
|
944
|
+
const origin = remotes.find((r) => r.name === "origin");
|
|
945
|
+
if (origin) {
|
|
946
|
+
logInfo(`Current remote: ${origin.refs.fetch}`);
|
|
947
|
+
} else {
|
|
948
|
+
logWarn("No remote configured");
|
|
949
|
+
}
|
|
950
|
+
const rl = createReadline();
|
|
951
|
+
try {
|
|
952
|
+
const url = await promptUrl(rl, "? New Git remote URL (leave empty to keep current): ");
|
|
953
|
+
if (!url) {
|
|
954
|
+
logInfo("Remote unchanged");
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
744
957
|
if (origin) {
|
|
745
|
-
|
|
958
|
+
await git.remote(["set-url", "origin", url]);
|
|
746
959
|
} else {
|
|
747
|
-
|
|
748
|
-
}
|
|
749
|
-
const rl2 = createReadline();
|
|
750
|
-
try {
|
|
751
|
-
const url = await rl2.question("? New Git remote URL (leave empty to keep current): ");
|
|
752
|
-
const trimmedUrl = url.trim();
|
|
753
|
-
if (!trimmedUrl) {
|
|
754
|
-
logInfo("Remote unchanged");
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
const gitUrlPattern = /^(https?:\/\/|git@|ssh:\/\/|git:\/\/).+/;
|
|
758
|
-
if (!gitUrlPattern.test(trimmedUrl)) {
|
|
759
|
-
logError("Invalid Git URL format. Expected https://, git@, ssh://, or git:// URL");
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
if (origin) {
|
|
763
|
-
await git.remote(["set-url", "origin", trimmedUrl]);
|
|
764
|
-
} else {
|
|
765
|
-
await git.addRemote("origin", trimmedUrl);
|
|
766
|
-
}
|
|
767
|
-
logOk(`Remote updated: ${trimmedUrl}`);
|
|
768
|
-
} finally {
|
|
769
|
-
rl2.close();
|
|
960
|
+
await git.addRemote("origin", url);
|
|
770
961
|
}
|
|
771
|
-
|
|
962
|
+
logOk(`Remote updated: ${url}`);
|
|
963
|
+
} finally {
|
|
964
|
+
rl.close();
|
|
772
965
|
}
|
|
966
|
+
}
|
|
967
|
+
async function freshInit() {
|
|
773
968
|
console.log("");
|
|
774
969
|
console.log("Welcome to vibe-sync! Let's set up config synchronization.");
|
|
775
970
|
console.log("");
|
|
776
971
|
const rl = createReadline();
|
|
777
972
|
try {
|
|
778
|
-
const url = await rl
|
|
779
|
-
|
|
780
|
-
if (!trimmedUrl) {
|
|
973
|
+
const url = await promptUrl(rl, "? Git remote URL: ");
|
|
974
|
+
if (!url) {
|
|
781
975
|
logError("URL cannot be empty");
|
|
782
976
|
return;
|
|
783
977
|
}
|
|
784
|
-
|
|
785
|
-
if (!gitUrlPattern.test(trimmedUrl)) {
|
|
786
|
-
logError("Invalid Git URL format. Expected https://, git@, ssh://, or git:// URL");
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
fs8.ensureDirSync(SYNC_DIR);
|
|
978
|
+
fs9.ensureDirSync(SYNC_DIR);
|
|
790
979
|
const git = createGit(SYNC_DIR);
|
|
791
980
|
await git.init();
|
|
792
981
|
logOk("Initialized git repository");
|
|
793
|
-
await git.addRemote("origin",
|
|
794
|
-
logOk(`Remote added: ${
|
|
982
|
+
await git.addRemote("origin", url);
|
|
983
|
+
logOk(`Remote added: ${url}`);
|
|
795
984
|
try {
|
|
796
|
-
await git
|
|
797
|
-
await git.branch(["--set-upstream-to=origin/main", "main"]);
|
|
798
|
-
logOk("Pulled existing data from remote");
|
|
985
|
+
await pullFromRemote(git);
|
|
799
986
|
} catch {
|
|
800
|
-
|
|
801
|
-
await git.pull("origin", "master");
|
|
802
|
-
await git.branch(["--set-upstream-to=origin/master", "master"]);
|
|
803
|
-
logOk("Pulled existing data from remote");
|
|
804
|
-
} catch {
|
|
805
|
-
logInfo("No existing data on remote (new repository)");
|
|
806
|
-
}
|
|
987
|
+
logInfo("No existing data on remote (new repository)");
|
|
807
988
|
}
|
|
808
989
|
const gitignorePath = path9.join(SYNC_DIR, ".gitignore");
|
|
809
|
-
if (!
|
|
810
|
-
|
|
990
|
+
if (!fs9.existsSync(gitignorePath)) {
|
|
991
|
+
fs9.writeFileSync(gitignorePath, ".DS_Store\nThumbs.db\n");
|
|
811
992
|
}
|
|
812
993
|
console.log("");
|
|
813
994
|
logOk("Setup complete!");
|
|
@@ -820,13 +1001,19 @@ async function cmdInit() {
|
|
|
820
1001
|
rl.close();
|
|
821
1002
|
}
|
|
822
1003
|
}
|
|
1004
|
+
async function cmdInit() {
|
|
1005
|
+
if (isInitialized()) {
|
|
1006
|
+
await updateRemote();
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
await freshInit();
|
|
1010
|
+
}
|
|
823
1011
|
|
|
824
1012
|
// src/commands/restore.ts
|
|
825
1013
|
function cmdRestore(timestamp) {
|
|
826
1014
|
const backups = listBackups();
|
|
827
1015
|
if (backups.length === 0) {
|
|
828
|
-
|
|
829
|
-
return;
|
|
1016
|
+
throw new Error('No backups found. Run "vibe-sync import" first to create a backup.');
|
|
830
1017
|
}
|
|
831
1018
|
if (!timestamp) {
|
|
832
1019
|
logInfo(`Available backups (${backups.length}):`);
|
|
@@ -840,15 +1027,14 @@ function cmdRestore(timestamp) {
|
|
|
840
1027
|
return;
|
|
841
1028
|
}
|
|
842
1029
|
if (!backups.includes(timestamp)) {
|
|
843
|
-
|
|
844
|
-
return;
|
|
1030
|
+
throw new Error(`Backup not found: ${timestamp}`);
|
|
845
1031
|
}
|
|
846
1032
|
restoreFromBackup(timestamp);
|
|
847
1033
|
}
|
|
848
1034
|
|
|
849
1035
|
// src/index.ts
|
|
850
1036
|
var program = new Command();
|
|
851
|
-
program.name("vibe-sync").description("Sync AI coding tool configurations across machines via git").version("0.
|
|
1037
|
+
program.name("vibe-sync").description("Sync AI coding tool configurations across machines via git").version("0.3.0").action(async () => {
|
|
852
1038
|
if (!isInitialized()) {
|
|
853
1039
|
await cmdInit();
|
|
854
1040
|
} else {
|