@tkpdx01/ccc 1.2.7 → 1.3.1
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/Claude Code settings.txt +776 -776
- package/README.md +77 -0
- package/index.js +4 -2
- package/package.json +40 -39
- package/src/commands/delete.js +66 -66
- package/src/commands/edit.js +120 -120
- package/src/commands/help.js +61 -50
- package/src/commands/import.js +356 -356
- package/src/commands/index.js +11 -10
- package/src/commands/list.js +46 -46
- package/src/commands/new.js +109 -109
- package/src/commands/show.js +68 -68
- package/src/commands/sync.js +93 -93
- package/src/commands/use.js +19 -19
- package/src/commands/webdav.js +477 -0
- package/src/config.js +9 -9
- package/src/crypto.js +147 -0
- package/src/launch.js +69 -69
- package/src/parsers.js +154 -154
- package/src/profiles.js +182 -182
- package/src/utils.js +67 -67
- package/src/webdav.js +268 -0
- package/.claude/settings.local.json +0 -34
- package/nul +0 -1
package/src/webdav.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { createClient } from 'webdav';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { CONFIG_DIR, PROFILES_DIR } from './config.js';
|
|
5
|
+
import { encrypt, decrypt } from './crypto.js';
|
|
6
|
+
import { getProfiles, readProfile, saveProfile } from './profiles.js';
|
|
7
|
+
|
|
8
|
+
const WEBDAV_CONFIG_FILE = path.join(CONFIG_DIR, 'webdav.json');
|
|
9
|
+
const REMOTE_FILE_NAME = 'ccc-profiles.encrypted';
|
|
10
|
+
|
|
11
|
+
// 读取 WebDAV 配置
|
|
12
|
+
export function getWebDAVConfig() {
|
|
13
|
+
if (!fs.existsSync(WEBDAV_CONFIG_FILE)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(fs.readFileSync(WEBDAV_CONFIG_FILE, 'utf-8'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 保存 WebDAV 配置
|
|
24
|
+
export function saveWebDAVConfig(config) {
|
|
25
|
+
fs.writeFileSync(WEBDAV_CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 创建 WebDAV 客户端
|
|
29
|
+
export function createWebDAVClient(config) {
|
|
30
|
+
return createClient(config.url, {
|
|
31
|
+
username: config.username,
|
|
32
|
+
password: config.password
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 获取远程文件路径
|
|
37
|
+
function getRemotePath(config) {
|
|
38
|
+
const basePath = config.path || '/';
|
|
39
|
+
return path.posix.join(basePath, REMOTE_FILE_NAME);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 上传加密的 profiles 到 WebDAV
|
|
43
|
+
export async function uploadProfiles(client, config, syncPassword) {
|
|
44
|
+
const profiles = getProfiles();
|
|
45
|
+
const profilesData = {};
|
|
46
|
+
|
|
47
|
+
for (const name of profiles) {
|
|
48
|
+
const profile = readProfile(name);
|
|
49
|
+
if (profile) {
|
|
50
|
+
profilesData[name] = {
|
|
51
|
+
data: profile,
|
|
52
|
+
updatedAt: Date.now()
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const payload = {
|
|
58
|
+
version: 1,
|
|
59
|
+
updatedAt: Date.now(),
|
|
60
|
+
profiles: profilesData
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const encrypted = encrypt(JSON.stringify(payload), syncPassword);
|
|
64
|
+
const remotePath = getRemotePath(config);
|
|
65
|
+
|
|
66
|
+
// 确保目录存在
|
|
67
|
+
const baseDir = config.path || '/';
|
|
68
|
+
try {
|
|
69
|
+
await client.createDirectory(baseDir, { recursive: true });
|
|
70
|
+
} catch {
|
|
71
|
+
// 目录可能已存在
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await client.putFileContents(remotePath, encrypted);
|
|
75
|
+
return { count: profiles.length, updatedAt: payload.updatedAt };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 从 WebDAV 下载加密的 profiles
|
|
79
|
+
export async function downloadProfiles(client, config, syncPassword) {
|
|
80
|
+
const remotePath = getRemotePath(config);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const exists = await client.exists(remotePath);
|
|
84
|
+
if (!exists) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const encrypted = await client.getFileContents(remotePath);
|
|
92
|
+
const decrypted = decrypt(Buffer.from(encrypted), syncPassword);
|
|
93
|
+
return JSON.parse(decrypted);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 获取远程文件信息
|
|
97
|
+
export async function getRemoteInfo(client, config, syncPassword) {
|
|
98
|
+
try {
|
|
99
|
+
const data = await downloadProfiles(client, config, syncPassword);
|
|
100
|
+
if (!data) {
|
|
101
|
+
return { exists: false };
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
exists: true,
|
|
105
|
+
updatedAt: data.updatedAt,
|
|
106
|
+
profileCount: Object.keys(data.profiles).length,
|
|
107
|
+
profileNames: Object.keys(data.profiles)
|
|
108
|
+
};
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return { exists: false, error: err.message };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 比较本地和远程 profiles,返回差异
|
|
115
|
+
export function compareProfiles(localProfiles, remoteData) {
|
|
116
|
+
const local = new Set(localProfiles);
|
|
117
|
+
const remote = new Set(remoteData ? Object.keys(remoteData.profiles) : []);
|
|
118
|
+
|
|
119
|
+
const result = {
|
|
120
|
+
localOnly: [], // 只在本地存在
|
|
121
|
+
remoteOnly: [], // 只在远程存在
|
|
122
|
+
both: [], // 两边都有
|
|
123
|
+
conflicts: [] // 两边都有且内容不同
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// 本地存在的
|
|
127
|
+
for (const name of local) {
|
|
128
|
+
if (remote.has(name)) {
|
|
129
|
+
result.both.push(name);
|
|
130
|
+
} else {
|
|
131
|
+
result.localOnly.push(name);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 只在远程存在的
|
|
136
|
+
for (const name of remote) {
|
|
137
|
+
if (!local.has(name)) {
|
|
138
|
+
result.remoteOnly.push(name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 检查内容是否相同
|
|
143
|
+
if (remoteData) {
|
|
144
|
+
for (const name of result.both) {
|
|
145
|
+
const localProfile = readProfile(name);
|
|
146
|
+
const remoteProfile = remoteData.profiles[name].data;
|
|
147
|
+
|
|
148
|
+
if (JSON.stringify(localProfile) !== JSON.stringify(remoteProfile)) {
|
|
149
|
+
result.conflicts.push({
|
|
150
|
+
name,
|
|
151
|
+
localData: localProfile,
|
|
152
|
+
remoteData: remoteProfile,
|
|
153
|
+
remoteUpdatedAt: remoteData.profiles[name].updatedAt
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 执行最大合并的 pull 操作
|
|
163
|
+
export function mergePull(remoteData, conflicts, resolutions) {
|
|
164
|
+
const imported = [];
|
|
165
|
+
const skipped = [];
|
|
166
|
+
const renamed = [];
|
|
167
|
+
|
|
168
|
+
if (!remoteData) {
|
|
169
|
+
return { imported, skipped, renamed };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const localProfiles = new Set(getProfiles());
|
|
173
|
+
|
|
174
|
+
for (const [name, profileData] of Object.entries(remoteData.profiles)) {
|
|
175
|
+
const conflict = conflicts.find(c => c.name === name);
|
|
176
|
+
|
|
177
|
+
if (conflict) {
|
|
178
|
+
// 有冲突
|
|
179
|
+
const resolution = resolutions[name] || 'keep_both';
|
|
180
|
+
|
|
181
|
+
if (resolution === 'use_remote') {
|
|
182
|
+
saveProfile(name, profileData.data);
|
|
183
|
+
imported.push(name);
|
|
184
|
+
} else if (resolution === 'keep_local') {
|
|
185
|
+
skipped.push(name);
|
|
186
|
+
} else {
|
|
187
|
+
// keep_both: 保留两者,远程版本重命名
|
|
188
|
+
const newName = `${name}_cloud`;
|
|
189
|
+
saveProfile(newName, profileData.data);
|
|
190
|
+
renamed.push({ original: name, renamed: newName });
|
|
191
|
+
}
|
|
192
|
+
} else if (!localProfiles.has(name)) {
|
|
193
|
+
// 本地不存在,直接导入
|
|
194
|
+
saveProfile(name, profileData.data);
|
|
195
|
+
imported.push(name);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { imported, skipped, renamed };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 执行最大合并的 push 操作
|
|
203
|
+
export async function mergePush(client, config, syncPassword, conflicts, resolutions) {
|
|
204
|
+
const localProfiles = getProfiles();
|
|
205
|
+
let remoteData = null;
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
remoteData = await downloadProfiles(client, config, syncPassword);
|
|
209
|
+
} catch {
|
|
210
|
+
// 远程可能不存在
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const profilesData = {};
|
|
214
|
+
|
|
215
|
+
// 保留远程独有的(最大合并)
|
|
216
|
+
if (remoteData) {
|
|
217
|
+
const localSet = new Set(localProfiles);
|
|
218
|
+
for (const [name, data] of Object.entries(remoteData.profiles)) {
|
|
219
|
+
if (!localSet.has(name)) {
|
|
220
|
+
profilesData[name] = data;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 添加本地的
|
|
226
|
+
for (const name of localProfiles) {
|
|
227
|
+
const profile = readProfile(name);
|
|
228
|
+
if (!profile) continue;
|
|
229
|
+
|
|
230
|
+
const conflict = conflicts.find(c => c.name === name);
|
|
231
|
+
if (conflict) {
|
|
232
|
+
const resolution = resolutions[name] || 'keep_both';
|
|
233
|
+
|
|
234
|
+
if (resolution === 'use_local') {
|
|
235
|
+
profilesData[name] = { data: profile, updatedAt: Date.now() };
|
|
236
|
+
} else if (resolution === 'keep_remote') {
|
|
237
|
+
// 保留远程版本
|
|
238
|
+
profilesData[name] = remoteData.profiles[name];
|
|
239
|
+
} else {
|
|
240
|
+
// keep_both: 保留两者,本地版本用 _local 后缀
|
|
241
|
+
profilesData[name] = remoteData.profiles[name]; // 保留原名为远程版本
|
|
242
|
+
profilesData[`${name}_local`] = { data: profile, updatedAt: Date.now() };
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
profilesData[name] = { data: profile, updatedAt: Date.now() };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const payload = {
|
|
250
|
+
version: 1,
|
|
251
|
+
updatedAt: Date.now(),
|
|
252
|
+
profiles: profilesData
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const encrypted = encrypt(JSON.stringify(payload), syncPassword);
|
|
256
|
+
const remotePath = getRemotePath(config);
|
|
257
|
+
|
|
258
|
+
// 确保目录存在
|
|
259
|
+
const baseDir = config.path || '/';
|
|
260
|
+
try {
|
|
261
|
+
await client.createDirectory(baseDir, { recursive: true });
|
|
262
|
+
} catch {
|
|
263
|
+
// 目录可能已存在
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await client.putFileContents(remotePath, encrypted);
|
|
267
|
+
return { count: Object.keys(profilesData).length };
|
|
268
|
+
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(cd:*)",
|
|
5
|
-
"Bash(del \"D:\\\\projects\\\\ccc\\\\README-CN.md\")",
|
|
6
|
-
"Bash(cat:*)",
|
|
7
|
-
"Bash(git tag:*)",
|
|
8
|
-
"Bash(git add:*)",
|
|
9
|
-
"Bash(git commit:*)",
|
|
10
|
-
"Bash(git push:*)",
|
|
11
|
-
"Bash(npm publish:*)",
|
|
12
|
-
"Bash(dir \"%USERPROFILE%\\\\.ccc\\\\profiles\")",
|
|
13
|
-
"Bash(git -C D:projectsccc status)",
|
|
14
|
-
"Bash(git -C D:projectsccc log -3 --oneline)",
|
|
15
|
-
"Bash(npm view:*)",
|
|
16
|
-
"Bash(where:*)",
|
|
17
|
-
"Bash(pnpm list:*)",
|
|
18
|
-
"Bash(node -e:*)",
|
|
19
|
-
"Bash(node:*)",
|
|
20
|
-
"Bash(findstr:*)",
|
|
21
|
-
"Bash(pnpm remove:*)",
|
|
22
|
-
"Bash(npm uninstall:*)",
|
|
23
|
-
"Bash(pnpm add:*)",
|
|
24
|
-
"Bash(dir:*)",
|
|
25
|
-
"Bash(del:*)",
|
|
26
|
-
"Bash(git -C D:/projects/ccc status)",
|
|
27
|
-
"Bash(git -C D:/projects/ccc add index.js package.json settings-sample.json src/commands/edit.js src/commands/help.js src/commands/import.js src/commands/index.js src/commands/list.js src/commands/new.js src/commands/show.js src/commands/sync.js src/utils.js)",
|
|
28
|
-
"Bash(git -C D:/projects/ccc commit -m \"$\\(cat <<''EOF''\nrefactor: simplify shadow config to only store API credentials\n\n- Remove env wrapper, use ANTHROPIC_AUTH_TOKEN and ANTHROPIC_BASE_URL directly at top level\n- Remove sync command \\(no longer needed\\)\n- Update all commands to use new simplified format\n- Bump version to 1.2.6\nEOF\n\\)\")",
|
|
29
|
-
"Bash(npm pack:*)",
|
|
30
|
-
"Bash(tar:*)",
|
|
31
|
-
"Bash(rm:*)"
|
|
32
|
-
]
|
|
33
|
-
}
|
|
34
|
-
}
|
package/nul
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
��Ϣ: ���ṩ��ģʽ���ҵ��ļ���
|