vibe-config-sync 0.2.4 → 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 +117 -9
- 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";
|
|
@@ -79,6 +82,11 @@ function writeJsonSafe(filePath, data) {
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
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
|
+
}
|
|
82
90
|
function sanitizePlugins(data) {
|
|
83
91
|
const result = structuredClone(data);
|
|
84
92
|
for (const entries of Object.values(result.plugins ?? {})) {
|
|
@@ -144,7 +152,21 @@ function importSkills(srcDir, destDir) {
|
|
|
144
152
|
}
|
|
145
153
|
|
|
146
154
|
// src/commands/export.ts
|
|
147
|
-
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() {
|
|
148
170
|
if (!fs4.existsSync(CLAUDE_HOME)) {
|
|
149
171
|
throw new Error(
|
|
150
172
|
`Claude config directory not found: ${CLAUDE_HOME}
|
|
@@ -196,6 +218,21 @@ Make sure Claude Code has been run at least once.`
|
|
|
196
218
|
);
|
|
197
219
|
logInfo("Exported: plugins/known_marketplaces.json (sanitized)");
|
|
198
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
|
+
}
|
|
199
236
|
logOk("Export complete");
|
|
200
237
|
}
|
|
201
238
|
|
|
@@ -234,6 +271,9 @@ function backupExisting() {
|
|
|
234
271
|
fs5.copySync(src, path5.join(backupDir, "plugins", file));
|
|
235
272
|
}
|
|
236
273
|
}
|
|
274
|
+
if (fs5.existsSync(CLAUDE_JSON)) {
|
|
275
|
+
fs5.copySync(CLAUDE_JSON, path5.join(backupDir, ".claude.json"));
|
|
276
|
+
}
|
|
237
277
|
logOk(`Backup created: ${backupDir}`);
|
|
238
278
|
}
|
|
239
279
|
function listBackups() {
|
|
@@ -283,6 +323,11 @@ function restoreFromBackup(backupName) {
|
|
|
283
323
|
}
|
|
284
324
|
}
|
|
285
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
|
+
}
|
|
286
331
|
logOk(`Restored from backup: ${backupName}`);
|
|
287
332
|
}
|
|
288
333
|
|
|
@@ -508,6 +553,49 @@ function importSyncSkills(configDir, dryRun) {
|
|
|
508
553
|
importSkills(skillsSrc, path6.join(CLAUDE_HOME, "skills"));
|
|
509
554
|
}
|
|
510
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
|
+
}
|
|
511
599
|
function syncPlugins(configDir, existingState, dryRun) {
|
|
512
600
|
const pluginsSrc = path6.join(configDir, "plugins");
|
|
513
601
|
const hasManifest = PLUGIN_FILES.some(
|
|
@@ -559,6 +647,7 @@ Run "vibe-sync export" first or "vibe-sync pull"`
|
|
|
559
647
|
importSyncFiles(configDir, options);
|
|
560
648
|
importSyncDirs(configDir, dryRun);
|
|
561
649
|
importSyncSkills(configDir, dryRun);
|
|
650
|
+
importMcpServers(configDir, dryRun);
|
|
562
651
|
if (options.reinstallPlugins) {
|
|
563
652
|
syncPlugins(configDir, existingState, dryRun);
|
|
564
653
|
} else {
|
|
@@ -568,6 +657,8 @@ Run "vibe-sync export" first or "vibe-sync pull"`
|
|
|
568
657
|
}
|
|
569
658
|
|
|
570
659
|
// src/commands/status.ts
|
|
660
|
+
import fs8 from "fs-extra";
|
|
661
|
+
import os2 from "os";
|
|
571
662
|
import path8 from "path";
|
|
572
663
|
|
|
573
664
|
// src/core/diff.ts
|
|
@@ -665,6 +756,23 @@ function cmdStatus() {
|
|
|
665
756
|
hasDiff = true;
|
|
666
757
|
}
|
|
667
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
|
+
}
|
|
668
776
|
if (!hasDiff) {
|
|
669
777
|
logOk("No differences found");
|
|
670
778
|
}
|
|
@@ -776,7 +884,7 @@ async function pullFromRemote(git) {
|
|
|
776
884
|
|
|
777
885
|
// src/commands/push.ts
|
|
778
886
|
async function cmdPush() {
|
|
779
|
-
cmdExport();
|
|
887
|
+
await cmdExport();
|
|
780
888
|
const git = createGit(SYNC_DIR);
|
|
781
889
|
await commitAndPush(git);
|
|
782
890
|
}
|
|
@@ -810,12 +918,12 @@ async function cmdPull(options = {}) {
|
|
|
810
918
|
}
|
|
811
919
|
|
|
812
920
|
// src/commands/init.ts
|
|
813
|
-
import
|
|
921
|
+
import fs9 from "fs-extra";
|
|
814
922
|
import path9 from "path";
|
|
815
|
-
import
|
|
923
|
+
import readline2 from "readline/promises";
|
|
816
924
|
var GIT_URL_PATTERN = /^(https?:\/\/|git@|ssh:\/\/|git:\/\/).+/;
|
|
817
925
|
function createReadline() {
|
|
818
|
-
return
|
|
926
|
+
return readline2.createInterface({
|
|
819
927
|
input: process.stdin,
|
|
820
928
|
output: process.stdout
|
|
821
929
|
});
|
|
@@ -867,7 +975,7 @@ async function freshInit() {
|
|
|
867
975
|
logError("URL cannot be empty");
|
|
868
976
|
return;
|
|
869
977
|
}
|
|
870
|
-
|
|
978
|
+
fs9.ensureDirSync(SYNC_DIR);
|
|
871
979
|
const git = createGit(SYNC_DIR);
|
|
872
980
|
await git.init();
|
|
873
981
|
logOk("Initialized git repository");
|
|
@@ -879,8 +987,8 @@ async function freshInit() {
|
|
|
879
987
|
logInfo("No existing data on remote (new repository)");
|
|
880
988
|
}
|
|
881
989
|
const gitignorePath = path9.join(SYNC_DIR, ".gitignore");
|
|
882
|
-
if (!
|
|
883
|
-
|
|
990
|
+
if (!fs9.existsSync(gitignorePath)) {
|
|
991
|
+
fs9.writeFileSync(gitignorePath, ".DS_Store\nThumbs.db\n");
|
|
884
992
|
}
|
|
885
993
|
console.log("");
|
|
886
994
|
logOk("Setup complete!");
|
|
@@ -926,7 +1034,7 @@ function cmdRestore(timestamp) {
|
|
|
926
1034
|
|
|
927
1035
|
// src/index.ts
|
|
928
1036
|
var program = new Command();
|
|
929
|
-
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 () => {
|
|
930
1038
|
if (!isInitialized()) {
|
|
931
1039
|
await cmdInit();
|
|
932
1040
|
} else {
|