wangchuan 5.3.0 → 5.6.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/.wangchuan/config.example.json +1 -1
- package/README.md +46 -10
- package/README.zh-CN.md +46 -10
- package/dist/bin/wangchuan.js +1 -1
- package/dist/src/agents/claude.d.ts.map +1 -1
- package/dist/src/agents/claude.js +5 -0
- package/dist/src/agents/claude.js.map +1 -1
- package/dist/src/agents/codebuddy.d.ts.map +1 -1
- package/dist/src/agents/codebuddy.js +2 -1
- package/dist/src/agents/codebuddy.js.map +1 -1
- package/dist/src/agents/codex.d.ts.map +1 -1
- package/dist/src/agents/codex.js +5 -0
- package/dist/src/agents/codex.js.map +1 -1
- package/dist/src/agents/cursor.d.ts.map +1 -1
- package/dist/src/agents/cursor.js +4 -1
- package/dist/src/agents/cursor.js.map +1 -1
- package/dist/src/agents/gemini.d.ts.map +1 -1
- package/dist/src/agents/gemini.js +4 -0
- package/dist/src/agents/gemini.js.map +1 -1
- package/dist/src/agents/workbuddy.d.ts.map +1 -1
- package/dist/src/agents/workbuddy.js +6 -1
- package/dist/src/agents/workbuddy.js.map +1 -1
- package/dist/src/commands/env.js +4 -0
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +67 -22
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/snapshot.d.ts.map +1 -1
- package/dist/src/commands/snapshot.js +7 -1
- package/dist/src/commands/snapshot.js.map +1 -1
- package/dist/src/commands/watch.js +1 -1
- package/dist/src/commands/watch.js.map +1 -1
- package/dist/src/core/config.js +1 -1
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/crypto.d.ts.map +1 -1
- package/dist/src/core/crypto.js +3 -2
- package/dist/src/core/crypto.js.map +1 -1
- package/dist/src/core/migrate.d.ts.map +1 -1
- package/dist/src/core/migrate.js +69 -8
- package/dist/src/core/migrate.js.map +1 -1
- package/dist/src/core/sync-restore.d.ts +19 -0
- package/dist/src/core/sync-restore.d.ts.map +1 -0
- package/dist/src/core/sync-restore.js +339 -0
- package/dist/src/core/sync-restore.js.map +1 -0
- package/dist/src/core/sync-shared.d.ts +30 -0
- package/dist/src/core/sync-shared.d.ts.map +1 -0
- package/dist/src/core/sync-shared.js +397 -0
- package/dist/src/core/sync-shared.js.map +1 -0
- package/dist/src/core/sync-stage.d.ts +57 -0
- package/dist/src/core/sync-stage.d.ts.map +1 -0
- package/dist/src/core/sync-stage.js +429 -0
- package/dist/src/core/sync-stage.js.map +1 -0
- package/dist/src/core/sync.d.ts +22 -46
- package/dist/src/core/sync.d.ts.map +1 -1
- package/dist/src/core/sync.js +64 -1267
- package/dist/src/core/sync.js.map +1 -1
- package/dist/src/i18n.d.ts.map +1 -1
- package/dist/src/i18n.js +31 -2
- package/dist/src/i18n.js.map +1 -1
- package/dist/test/crypto.test.js +2 -2
- package/dist/test/crypto.test.js.map +1 -1
- package/dist/test/git.test.d.ts +5 -0
- package/dist/test/git.test.d.ts.map +1 -0
- package/dist/test/git.test.js +90 -0
- package/dist/test/git.test.js.map +1 -0
- package/dist/test/migrate.test.d.ts +9 -0
- package/dist/test/migrate.test.d.ts.map +1 -0
- package/dist/test/migrate.test.js +133 -0
- package/dist/test/migrate.test.js.map +1 -0
- package/dist/test/sync-lock.test.d.ts +5 -0
- package/dist/test/sync-lock.test.d.ts.map +1 -0
- package/dist/test/sync-lock.test.js +94 -0
- package/dist/test/sync-lock.test.js.map +1 -0
- package/package.json +2 -2
- package/skill/SKILL.md +81 -9
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* migrate.test.ts — ensureMigrated unit tests
|
|
3
|
+
*
|
|
4
|
+
* Tests profile reconciliation behavior: verifies that user customizations
|
|
5
|
+
* (enabled state, workspacePath) are preserved while sync definitions are
|
|
6
|
+
* updated from latest agent definitions.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, after, before } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { ensureMigrated } from '../src/core/migrate.js';
|
|
14
|
+
import { config, CONFIG_VERSION } from '../src/core/config.js';
|
|
15
|
+
import { buildDefaultProfiles, buildDefaultShared } from '../src/agents/index.js';
|
|
16
|
+
import { AGENT_NAMES } from '../src/types.js';
|
|
17
|
+
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'wc-test-migrate-'));
|
|
18
|
+
// Create a synthetic ~/.wangchuan structure for config.save() calls inside ensureMigrated
|
|
19
|
+
const FAKE_HOME = path.join(TMP, 'home');
|
|
20
|
+
const WANGCHUAN_DIR = path.join(FAKE_HOME, '.wangchuan');
|
|
21
|
+
const REPO_DIR = path.join(WANGCHUAN_DIR, 'repo');
|
|
22
|
+
// Backup real paths and override
|
|
23
|
+
const REAL_SAVE = config.save.bind(config);
|
|
24
|
+
let savedCfg = null;
|
|
25
|
+
before(() => {
|
|
26
|
+
fs.mkdirSync(REPO_DIR, { recursive: true });
|
|
27
|
+
fs.mkdirSync(path.join(REPO_DIR, 'agents'), { recursive: true });
|
|
28
|
+
// Monkey-patch config.save to capture output without writing to real ~/.wangchuan
|
|
29
|
+
config.save = (cfg) => {
|
|
30
|
+
savedCfg = cfg;
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
after(() => {
|
|
34
|
+
// Restore real save
|
|
35
|
+
config.save = REAL_SAVE;
|
|
36
|
+
fs.rmSync(TMP, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
/** Build a minimal v2 config for testing */
|
|
39
|
+
function buildTestConfig(profileOverrides) {
|
|
40
|
+
const defaults = buildDefaultProfiles();
|
|
41
|
+
const profiles = {};
|
|
42
|
+
for (const name of AGENT_NAMES) {
|
|
43
|
+
const def = defaults[name];
|
|
44
|
+
const override = profileOverrides?.[name];
|
|
45
|
+
profiles[name] = { ...def, ...override };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
repo: 'git@github.com:test/test.git',
|
|
49
|
+
branch: 'main',
|
|
50
|
+
localRepoPath: REPO_DIR,
|
|
51
|
+
keyPath: path.join(WANGCHUAN_DIR, 'master.key'),
|
|
52
|
+
hostname: 'test-host',
|
|
53
|
+
version: CONFIG_VERSION,
|
|
54
|
+
profiles: { default: profiles },
|
|
55
|
+
shared: buildDefaultShared(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
describe('ensureMigrated — v2 config reconciliation', () => {
|
|
59
|
+
it('already-current v2 config passes through without error', () => {
|
|
60
|
+
const cfg = buildTestConfig();
|
|
61
|
+
const result = ensureMigrated(cfg);
|
|
62
|
+
assert.equal(result.version, CONFIG_VERSION);
|
|
63
|
+
});
|
|
64
|
+
it('preserves user enabled=true when reconciling', () => {
|
|
65
|
+
const cfg = buildTestConfig({ claude: { enabled: true } });
|
|
66
|
+
const result = ensureMigrated(cfg);
|
|
67
|
+
const claude = result.profiles.default.claude;
|
|
68
|
+
assert.equal(claude.enabled, true);
|
|
69
|
+
});
|
|
70
|
+
it('preserves user enabled=false when reconciling', () => {
|
|
71
|
+
const cfg = buildTestConfig({ openclaw: { enabled: false } });
|
|
72
|
+
const result = ensureMigrated(cfg);
|
|
73
|
+
const oc = result.profiles.default.openclaw;
|
|
74
|
+
assert.equal(oc.enabled, false);
|
|
75
|
+
});
|
|
76
|
+
it('preserves user workspacePath customization', () => {
|
|
77
|
+
const customPath = '/custom/workspace/path';
|
|
78
|
+
const cfg = buildTestConfig({ gemini: { workspacePath: customPath } });
|
|
79
|
+
const result = ensureMigrated(cfg);
|
|
80
|
+
assert.equal(result.profiles.default.gemini.workspacePath, customPath);
|
|
81
|
+
});
|
|
82
|
+
it('updates syncFiles from latest definitions', () => {
|
|
83
|
+
const defaults = buildDefaultProfiles();
|
|
84
|
+
const latestClaudeFiles = defaults.claude.syncFiles;
|
|
85
|
+
// Start with an outdated syncFiles (empty array)
|
|
86
|
+
const cfg = buildTestConfig({ claude: { syncFiles: [] } });
|
|
87
|
+
const result = ensureMigrated(cfg);
|
|
88
|
+
assert.deepEqual(result.profiles.default.claude.syncFiles, latestClaudeFiles);
|
|
89
|
+
});
|
|
90
|
+
it('result contains all agent names', () => {
|
|
91
|
+
const cfg = buildTestConfig();
|
|
92
|
+
const result = ensureMigrated(cfg);
|
|
93
|
+
for (const name of AGENT_NAMES) {
|
|
94
|
+
assert.ok(result.profiles.default[name] !== undefined, `agent ${name} should be present in reconciled profiles`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
it('result has shared config', () => {
|
|
98
|
+
const cfg = buildTestConfig();
|
|
99
|
+
const result = ensureMigrated(cfg);
|
|
100
|
+
assert.ok(result.shared !== undefined);
|
|
101
|
+
assert.ok(result.shared.skills !== undefined);
|
|
102
|
+
assert.ok(result.shared.mcp !== undefined);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('ensureMigrated — new agent detection', () => {
|
|
106
|
+
it('adds new agents from definitions with enabled=false', () => {
|
|
107
|
+
// Build a config missing the last agent to simulate an upgrade
|
|
108
|
+
const defaults = buildDefaultProfiles();
|
|
109
|
+
const profiles = {};
|
|
110
|
+
const allNames = [...AGENT_NAMES];
|
|
111
|
+
const removedAgent = allNames[allNames.length - 1];
|
|
112
|
+
for (const name of allNames) {
|
|
113
|
+
if (name === removedAgent)
|
|
114
|
+
continue;
|
|
115
|
+
profiles[name] = defaults[name];
|
|
116
|
+
}
|
|
117
|
+
const cfg = {
|
|
118
|
+
repo: 'git@github.com:test/test.git',
|
|
119
|
+
branch: 'main',
|
|
120
|
+
localRepoPath: REPO_DIR,
|
|
121
|
+
keyPath: path.join(WANGCHUAN_DIR, 'master.key'),
|
|
122
|
+
hostname: 'test-host',
|
|
123
|
+
version: CONFIG_VERSION,
|
|
124
|
+
profiles: { default: profiles },
|
|
125
|
+
shared: buildDefaultShared(),
|
|
126
|
+
};
|
|
127
|
+
const result = ensureMigrated(cfg);
|
|
128
|
+
const added = result.profiles.default[removedAgent];
|
|
129
|
+
assert.ok(added !== undefined, `${removedAgent} should be added`);
|
|
130
|
+
assert.equal(added.enabled, false, 'newly added agent should be disabled');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
//# sourceMappingURL=migrate.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.test.js","sourceRoot":"","sources":["../../test/migrate.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-lock.test.d.ts","sourceRoot":"","sources":["../../test/sync-lock.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sync-lock.test.ts — syncLock unit tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, after } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import { syncLock } from '../src/core/sync-lock.js';
|
|
11
|
+
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'wc-test-lock-'));
|
|
12
|
+
// Create a bare repo + work clone so acquire() has a valid repoPath for cleanDirtyState
|
|
13
|
+
const BARE = path.join(TMP, 'bare.git');
|
|
14
|
+
const REPO = path.join(TMP, 'repo');
|
|
15
|
+
execSync(`git init --bare "${BARE}"`);
|
|
16
|
+
// Use a seed clone to create initial commit, then re-clone for REPO
|
|
17
|
+
const SEED = path.join(TMP, 'seed');
|
|
18
|
+
execSync(`git clone "${BARE}" "${SEED}"`);
|
|
19
|
+
execSync('git config user.name "Test"', { cwd: SEED });
|
|
20
|
+
execSync('git config user.email "test@test.com"', { cwd: SEED });
|
|
21
|
+
fs.writeFileSync(path.join(SEED, 'init.txt'), 'seed');
|
|
22
|
+
execSync('git add . && git commit -m "seed"', { cwd: SEED });
|
|
23
|
+
// Push to whatever default branch was created
|
|
24
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: SEED, encoding: 'utf-8' }).trim();
|
|
25
|
+
execSync(`git push origin ${branch}`, { cwd: SEED });
|
|
26
|
+
// Clone fresh for test use
|
|
27
|
+
execSync(`git clone "${BARE}" "${REPO}"`);
|
|
28
|
+
after(() => {
|
|
29
|
+
// Always clean up lock to avoid polluting the real ~/.wangchuan/
|
|
30
|
+
syncLock.release();
|
|
31
|
+
fs.rmSync(TMP, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
describe('syncLock.acquire + release', () => {
|
|
34
|
+
it('basic cycle: lock file created then deleted', async () => {
|
|
35
|
+
await syncLock.acquire(REPO);
|
|
36
|
+
assert.ok(fs.existsSync(syncLock.lockPath), 'lock file should exist after acquire');
|
|
37
|
+
syncLock.release();
|
|
38
|
+
assert.ok(!fs.existsSync(syncLock.lockPath), 'lock file should be gone after release');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('syncLock.exists', () => {
|
|
42
|
+
it('returns true when lock present', async () => {
|
|
43
|
+
await syncLock.acquire(REPO);
|
|
44
|
+
assert.equal(syncLock.exists(), true);
|
|
45
|
+
syncLock.release();
|
|
46
|
+
});
|
|
47
|
+
it('returns false when absent', () => {
|
|
48
|
+
assert.equal(syncLock.exists(), false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('syncLock.read', () => {
|
|
52
|
+
it('returns parsed lock with pid and startedAt', async () => {
|
|
53
|
+
await syncLock.acquire(REPO);
|
|
54
|
+
const lock = syncLock.read();
|
|
55
|
+
assert.ok(lock !== null);
|
|
56
|
+
assert.equal(lock.pid, process.pid);
|
|
57
|
+
assert.ok(typeof lock.startedAt === 'string');
|
|
58
|
+
syncLock.release();
|
|
59
|
+
});
|
|
60
|
+
it('returns null for corrupt lock file', () => {
|
|
61
|
+
// Write garbage to the lock path
|
|
62
|
+
fs.mkdirSync(path.dirname(syncLock.lockPath), { recursive: true });
|
|
63
|
+
fs.writeFileSync(syncLock.lockPath, '{{{not json', 'utf-8');
|
|
64
|
+
const lock = syncLock.read();
|
|
65
|
+
assert.equal(lock, null);
|
|
66
|
+
fs.unlinkSync(syncLock.lockPath);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('syncLock.acquire stale detection', () => {
|
|
70
|
+
it('cleans stale lock from dead PID', async () => {
|
|
71
|
+
// Write a lock with a PID that almost certainly does not exist
|
|
72
|
+
fs.mkdirSync(path.dirname(syncLock.lockPath), { recursive: true });
|
|
73
|
+
fs.writeFileSync(syncLock.lockPath, JSON.stringify({
|
|
74
|
+
startedAt: new Date().toISOString(),
|
|
75
|
+
pid: 999999,
|
|
76
|
+
}), 'utf-8');
|
|
77
|
+
// acquire should detect dead PID and succeed
|
|
78
|
+
await syncLock.acquire(REPO);
|
|
79
|
+
assert.ok(syncLock.exists());
|
|
80
|
+
const lock = syncLock.read();
|
|
81
|
+
assert.equal(lock.pid, process.pid);
|
|
82
|
+
syncLock.release();
|
|
83
|
+
});
|
|
84
|
+
it('throws when live PID holds lock', async () => {
|
|
85
|
+
await syncLock.acquire(REPO);
|
|
86
|
+
// Attempting a second acquire with live PID should throw
|
|
87
|
+
await assert.rejects(() => syncLock.acquire(REPO), (err) => {
|
|
88
|
+
assert.ok(err.message.includes(String(process.pid)));
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
syncLock.release();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
//# sourceMappingURL=sync-lock.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-lock.test.js","sourceRoot":"","sources":["../../test/sync-lock.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wangchuan",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.0",
|
|
4
4
|
"description": "忘川 · AI 记忆同步系统 — 智能体记忆永不遗失",
|
|
5
5
|
"bin": {
|
|
6
6
|
"wangchuan": "./dist/bin/wangchuan.js"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"postbuild": "chmod +x dist/bin/wangchuan.js",
|
|
18
18
|
"prepublishOnly": "npm run build",
|
|
19
19
|
"dev": "tsx bin/wangchuan.ts",
|
|
20
|
-
"test": "node --import tsx/esm --test test/crypto.test.ts test/json-field.test.ts test/sync.test.ts test/sync-history.test.ts test/merge.test.ts",
|
|
20
|
+
"test": "node --import tsx/esm --test test/crypto.test.ts test/json-field.test.ts test/sync.test.ts test/sync-history.test.ts test/merge.test.ts test/git.test.ts test/sync-lock.test.ts test/migrate.test.ts",
|
|
21
21
|
"typecheck": "tsc --noEmit"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
package/skill/SKILL.md
CHANGED
|
@@ -113,12 +113,12 @@ Supported by: `sync`, `status`, `watch`, `memory`.
|
|
|
113
113
|
| Value | Description |
|
|
114
114
|
|-------|-------------|
|
|
115
115
|
| `openclaw` | MEMORY.md (enc), AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md (enc), HEARTBEAT.md, BOOTSTRAP.md, memory/ (enc), openclaw.json → agents/skills/ui (enc), skills/ |
|
|
116
|
-
| `claude` | CLAUDE.md, settings.json (enc), .claude.json → mcpServers (enc) |
|
|
117
|
-
| `gemini` | settings.internal.json → security + model + general (enc) |
|
|
118
|
-
| `codebuddy` | MEMORY.md (enc), CODEBUDDY.md, mcp.json → mcpServers (enc), settings.json → enabledPlugins (enc) |
|
|
119
|
-
| `workbuddy` | MEMORY.md (enc), IDENTITY.md, SOUL.md, USER.md (enc), BOOTSTRAP.md, mcp.json → mcpServers (enc), settings.json → enabledPlugins (enc) |
|
|
120
|
-
| `cursor` | rules/ dir, mcp.json → mcpServers (enc), cli-config.json → permissions + model + enabledPlugins (enc) |
|
|
121
|
-
| `codex` |
|
|
116
|
+
| `claude` | CLAUDE.md, settings.json (enc), .claude.json → mcpServers (enc), commands/ (dir), plugins/ (installed + marketplaces) |
|
|
117
|
+
| `gemini` | settings.internal.json → security + model + general (enc), skills/ (dir) |
|
|
118
|
+
| `codebuddy` | MEMORY.md (enc), CODEBUDDY.md, mcp.json → mcpServers (enc), settings.json → enabledPlugins + hooks (enc), plugins/ (marketplaces) |
|
|
119
|
+
| `workbuddy` | MEMORY.md (enc), IDENTITY.md, SOUL.md, USER.md (enc), BOOTSTRAP.md, mcp.json → mcpServers (enc), settings.json → enabledPlugins + hooks (enc), skills/ (dir), extensions/ |
|
|
120
|
+
| `cursor` | rules/ dir, mcp.json → mcpServers (enc), cli-config.json → permissions + model + enabledPlugins (enc), extensions/, hooks.json |
|
|
121
|
+
| `codex` | MEMORY.md (enc), instructions.md, config.toml (enc), skills/ (dir), memories/ (enc) |
|
|
122
122
|
|
|
123
123
|
When omitted, operates on all enabled agents plus the shared tier (skills/MCP/custom agents/shared memory).
|
|
124
124
|
|
|
@@ -162,6 +162,78 @@ The watch daemon auto-detects file changes and triggers sync, serving as a safet
|
|
|
162
162
|
## Prerequisites
|
|
163
163
|
|
|
164
164
|
1. Node.js ≥ 18
|
|
165
|
-
2.
|
|
166
|
-
3.
|
|
167
|
-
|
|
165
|
+
2. Git installed and configured (SSH key or HTTPS credentials)
|
|
166
|
+
3. A **private** Git repo on any hosting platform (GitHub, GitLab, Gitee, Bitbucket, Gitea, or self-hosted)
|
|
167
|
+
|
|
168
|
+
## Installation
|
|
169
|
+
|
|
170
|
+
### Install wangchuan CLI
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npm install -g wangchuan
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### First-time setup
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Interactive — auto-detects installed agents, creates repo via GitHub CLI if available
|
|
180
|
+
wangchuan init
|
|
181
|
+
|
|
182
|
+
# Or specify a repo URL directly (any Git hosting):
|
|
183
|
+
wangchuan init --repo git@github.com:you/brain.git
|
|
184
|
+
wangchuan init --repo git@gitlab.com:you/brain.git
|
|
185
|
+
wangchuan init --repo git@gitee.com:you/brain.git
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Install this skill to an agent
|
|
189
|
+
|
|
190
|
+
Copy the `wangchuan/` skill folder to your agent's skills directory:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Claude
|
|
194
|
+
cp -r wangchuan/ ~/.claude/skills/wangchuan/
|
|
195
|
+
|
|
196
|
+
# OpenClaw
|
|
197
|
+
cp -r wangchuan/ ~/.openclaw/workspace/skills/wangchuan/
|
|
198
|
+
|
|
199
|
+
# Codex
|
|
200
|
+
cp -r wangchuan/ ~/.codex/skills/wangchuan/
|
|
201
|
+
|
|
202
|
+
# Or let wangchuan sync distribute it to all agents automatically:
|
|
203
|
+
wangchuan sync
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Setting up Git repo (if you don't have one)
|
|
207
|
+
|
|
208
|
+
Create a **private** repo on your preferred platform:
|
|
209
|
+
|
|
210
|
+
| Platform | How to create |
|
|
211
|
+
|----------|--------------|
|
|
212
|
+
| **GitHub** | `wangchuan init` auto-creates via `gh` CLI, or: github.com → New repository → Private |
|
|
213
|
+
| **GitLab** | gitlab.com → New project → Private |
|
|
214
|
+
| **Gitee** | gitee.com → New repo → Private |
|
|
215
|
+
| **Bitbucket** | bitbucket.org → Create repository → Private |
|
|
216
|
+
| **Gitea** | Your instance → New Repository → Private |
|
|
217
|
+
|
|
218
|
+
Then: `wangchuan init --repo <ssh-url>`
|
|
219
|
+
|
|
220
|
+
### New machine setup
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
npm install -g wangchuan
|
|
224
|
+
wangchuan init --repo <your-repo-url> --key <master-key-hex>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Get the master key from your original machine: `wangchuan doctor --key-export`
|
|
228
|
+
|
|
229
|
+
### Migrating the master key
|
|
230
|
+
|
|
231
|
+
⚠️ **`master.key` is the ONLY thing that cannot be recovered.** Back it up securely.
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# On the source machine:
|
|
235
|
+
wangchuan doctor --key-export # prints wangchuan_<hex>
|
|
236
|
+
|
|
237
|
+
# On the target machine:
|
|
238
|
+
wangchuan init --repo <url> --key wangchuan_<hex>
|
|
239
|
+
```
|