ccpanel 0.1.2 → 0.1.3
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/dist/{chunk-7ROXP6AO.js → chunk-SZFZF7CH.js} +52 -12
- package/dist/cli.js +28 -16
- package/dist/server/app.js +1 -1
- package/package.json +2 -1
- package/web/dist/assets/{index-DoKBz_TC.js → index-BGhhpb6e.js} +26 -26
- package/web/dist/assets/index-DZh_OBPf.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CscUfGaI.css +0 -1
|
@@ -50,6 +50,27 @@ var ConfigStore = class {
|
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
// src/core/errors.ts
|
|
54
|
+
import { stat } from "fs/promises";
|
|
55
|
+
var ConflictError = class extends Error {
|
|
56
|
+
constructor(message = "\u6587\u4EF6\u5DF2\u88AB\u5916\u90E8\u4FEE\u6539\uFF0C\u8BF7\u91CD\u65B0\u52A0\u8F7D\u540E\u518D\u4FDD\u5B58") {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "ConflictError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
async function currentMtime(file) {
|
|
62
|
+
try {
|
|
63
|
+
return (await stat(file)).mtimeMs;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.code === "ENOENT") return null;
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function assertNoConflict(file, baseMtime) {
|
|
70
|
+
if (baseMtime === void 0) return;
|
|
71
|
+
if (await currentMtime(file) !== baseMtime) throw new ConflictError();
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
// src/server/routes/mcp.ts
|
|
54
75
|
import { Hono } from "hono";
|
|
55
76
|
|
|
@@ -110,10 +131,20 @@ var McpServerInputSchema = z.intersection(
|
|
|
110
131
|
McpServerSchema
|
|
111
132
|
);
|
|
112
133
|
var SkillFrontmatterSchema = z.object({ name: z.string().min(1), description: z.string().min(1) }).passthrough();
|
|
113
|
-
var
|
|
114
|
-
var
|
|
134
|
+
var BaseMtimeSchema = z.number().nullable().optional();
|
|
135
|
+
var SkillFileWriteSchema = z.object({
|
|
136
|
+
content: z.string(),
|
|
137
|
+
baseMtime: BaseMtimeSchema
|
|
138
|
+
});
|
|
139
|
+
var MemoryWriteSchema = z.object({
|
|
140
|
+
content: z.string(),
|
|
141
|
+
baseMtime: BaseMtimeSchema
|
|
142
|
+
});
|
|
115
143
|
var ConfigFileIdSchema = z.enum(["settings", "claude-json"]);
|
|
116
|
-
var ConfigFileWriteSchema = z.object({
|
|
144
|
+
var ConfigFileWriteSchema = z.object({
|
|
145
|
+
content: z.string(),
|
|
146
|
+
baseMtime: BaseMtimeSchema
|
|
147
|
+
});
|
|
117
148
|
var ScopeQuerySchema = z.object({
|
|
118
149
|
scope: ScopeSchema,
|
|
119
150
|
projectPath: z.string().optional()
|
|
@@ -205,13 +236,14 @@ function skillRoutes(deps) {
|
|
|
205
236
|
});
|
|
206
237
|
app.put("/:name/files/:filePath{.+}", async (c) => {
|
|
207
238
|
const { q, paths } = ctx(c);
|
|
208
|
-
const { content } = SkillFileWriteSchema.parse(await c.req.json());
|
|
239
|
+
const { content, baseMtime } = SkillFileWriteSchema.parse(await c.req.json());
|
|
209
240
|
await deps.skills.writeFileRaw(
|
|
210
241
|
paths,
|
|
211
242
|
q.scope,
|
|
212
243
|
c.req.param("name"),
|
|
213
244
|
c.req.param("filePath"),
|
|
214
|
-
content
|
|
245
|
+
content,
|
|
246
|
+
baseMtime
|
|
215
247
|
);
|
|
216
248
|
return c.json({ ok: true });
|
|
217
249
|
});
|
|
@@ -275,8 +307,8 @@ function memoryRoutes(deps) {
|
|
|
275
307
|
});
|
|
276
308
|
app.put("/", async (c) => {
|
|
277
309
|
const { q, paths } = ctx(c);
|
|
278
|
-
const { content } = MemoryWriteSchema.parse(await c.req.json());
|
|
279
|
-
await deps.memory.write(paths, q.scope, content);
|
|
310
|
+
const { content, baseMtime } = MemoryWriteSchema.parse(await c.req.json());
|
|
311
|
+
await deps.memory.write(paths, q.scope, content, baseMtime);
|
|
280
312
|
return c.json({ ok: true });
|
|
281
313
|
});
|
|
282
314
|
return app;
|
|
@@ -380,8 +412,8 @@ function configFileRoutes(deps) {
|
|
|
380
412
|
});
|
|
381
413
|
app.put("/:file", async (c) => {
|
|
382
414
|
const { fileId, q, paths } = ctx(c);
|
|
383
|
-
const { content } = ConfigFileWriteSchema.parse(await c.req.json());
|
|
384
|
-
await deps.configFiles.write(paths, fileId, q.scope, content);
|
|
415
|
+
const { content, baseMtime } = ConfigFileWriteSchema.parse(await c.req.json());
|
|
416
|
+
await deps.configFiles.write(paths, fileId, q.scope, content, baseMtime);
|
|
385
417
|
return c.json({ ok: true });
|
|
386
418
|
});
|
|
387
419
|
return app;
|
|
@@ -391,7 +423,10 @@ function configFileRoutes(deps) {
|
|
|
391
423
|
import { Hono as Hono9 } from "hono";
|
|
392
424
|
import { z as z7 } from "zod";
|
|
393
425
|
var CreateSchema = z7.object({ name: z7.string().min(1), content: z7.string() });
|
|
394
|
-
var WriteSchema = z7.object({
|
|
426
|
+
var WriteSchema = z7.object({
|
|
427
|
+
content: z7.string(),
|
|
428
|
+
baseMtime: z7.number().nullable().optional()
|
|
429
|
+
});
|
|
395
430
|
function agentCommandRoutes(deps, kind) {
|
|
396
431
|
const app = new Hono9();
|
|
397
432
|
const ctx = (c) => {
|
|
@@ -417,8 +452,8 @@ function agentCommandRoutes(deps, kind) {
|
|
|
417
452
|
});
|
|
418
453
|
app.put("/:name", async (c) => {
|
|
419
454
|
const { q, paths } = ctx(c);
|
|
420
|
-
const { content } = WriteSchema.parse(await c.req.json());
|
|
421
|
-
await deps.agentCommands.write(paths, kind, q.scope, c.req.param("name"), content);
|
|
455
|
+
const { content, baseMtime } = WriteSchema.parse(await c.req.json());
|
|
456
|
+
await deps.agentCommands.write(paths, kind, q.scope, c.req.param("name"), content, baseMtime);
|
|
422
457
|
return c.json({ ok: true });
|
|
423
458
|
});
|
|
424
459
|
app.delete("/:name", async (c) => {
|
|
@@ -442,6 +477,9 @@ function createApp(deps) {
|
|
|
442
477
|
if (err instanceof InvalidJsonError) {
|
|
443
478
|
return c.json({ error: err.message }, 400);
|
|
444
479
|
}
|
|
480
|
+
if (err instanceof ConflictError) {
|
|
481
|
+
return c.json({ error: err.message, code: "conflict" }, 409);
|
|
482
|
+
}
|
|
445
483
|
return c.json({ error: err.message }, 500);
|
|
446
484
|
});
|
|
447
485
|
app.route("/api/mcp", mcpRoutes(deps));
|
|
@@ -461,6 +499,8 @@ function createApp(deps) {
|
|
|
461
499
|
export {
|
|
462
500
|
InvalidJsonError,
|
|
463
501
|
ConfigStore,
|
|
502
|
+
currentMtime,
|
|
503
|
+
assertNoConflict,
|
|
464
504
|
SkillFrontmatterSchema,
|
|
465
505
|
createApp
|
|
466
506
|
};
|
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,10 @@ import {
|
|
|
3
3
|
ConfigStore,
|
|
4
4
|
InvalidJsonError,
|
|
5
5
|
SkillFrontmatterSchema,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
assertNoConflict,
|
|
7
|
+
createApp,
|
|
8
|
+
currentMtime
|
|
9
|
+
} from "./chunk-SZFZF7CH.js";
|
|
8
10
|
|
|
9
11
|
// src/cli.ts
|
|
10
12
|
import { homedir, networkInterfaces } from "os";
|
|
@@ -196,17 +198,19 @@ var SkillService = class {
|
|
|
196
198
|
continue;
|
|
197
199
|
}
|
|
198
200
|
}
|
|
201
|
+
const mtime = await currentMtime(join(path, "SKILL.md")) ?? await currentMtime(path) ?? 0;
|
|
199
202
|
try {
|
|
200
203
|
const raw = await readFile(join(path, "SKILL.md"), "utf8");
|
|
201
204
|
const fm = SkillFrontmatterSchema.parse(matter(raw).data);
|
|
202
|
-
out.push({ name: e.name, description: fm.description, enabled, path, linkTarget });
|
|
205
|
+
out.push({ name: e.name, description: fm.description, enabled, path, linkTarget, mtime });
|
|
203
206
|
} catch {
|
|
204
207
|
out.push({
|
|
205
208
|
name: e.name,
|
|
206
209
|
description: "(SKILL.md \u7F3A\u5931\u6216\u65E0\u6548)",
|
|
207
210
|
enabled,
|
|
208
211
|
path,
|
|
209
|
-
linkTarget
|
|
212
|
+
linkTarget,
|
|
213
|
+
mtime
|
|
210
214
|
});
|
|
211
215
|
}
|
|
212
216
|
}
|
|
@@ -284,11 +288,12 @@ var SkillService = class {
|
|
|
284
288
|
const full = this.resolveWithin(root, relPath);
|
|
285
289
|
const buf = await readFile(full);
|
|
286
290
|
if (isBinary(buf)) throw new Error(`\u4E8C\u8FDB\u5236\u6587\u4EF6\u65E0\u6CD5\u7F16\u8F91\uFF1A${relPath}`);
|
|
287
|
-
return { content: buf.toString("utf8") };
|
|
291
|
+
return { content: buf.toString("utf8"), mtime: await currentMtime(full) };
|
|
288
292
|
}
|
|
289
|
-
async writeFileRaw(paths, scope, name, relPath, content) {
|
|
293
|
+
async writeFileRaw(paths, scope, name, relPath, content, baseMtime) {
|
|
290
294
|
const root = await this.resolveSkillDir(paths, scope, name);
|
|
291
295
|
const full = this.resolveWithin(root, relPath);
|
|
296
|
+
await assertNoConflict(full, baseMtime);
|
|
292
297
|
await mkdir(dirname(full), { recursive: true });
|
|
293
298
|
await writeFile(full, content, "utf8");
|
|
294
299
|
}
|
|
@@ -376,17 +381,20 @@ var MemoryService = class {
|
|
|
376
381
|
if (!f) throw new Error("\u7F3A\u5C11\u9879\u76EE CLAUDE.md \u8DEF\u5F84");
|
|
377
382
|
return f;
|
|
378
383
|
}
|
|
379
|
-
//
|
|
384
|
+
// 文件不存在视为「尚未创建」,返回空串与 null mtime。
|
|
380
385
|
async read(paths, scope) {
|
|
386
|
+
const f = this.file(paths, scope);
|
|
381
387
|
try {
|
|
382
|
-
return { content: await readFile2(
|
|
388
|
+
return { content: await readFile2(f, "utf8"), mtime: await currentMtime(f) };
|
|
383
389
|
} catch (err) {
|
|
384
|
-
if (err.code === "ENOENT")
|
|
390
|
+
if (err.code === "ENOENT")
|
|
391
|
+
return { content: "", mtime: null };
|
|
385
392
|
throw err;
|
|
386
393
|
}
|
|
387
394
|
}
|
|
388
|
-
async write(paths, scope, content) {
|
|
395
|
+
async write(paths, scope, content, baseMtime) {
|
|
389
396
|
const f = this.file(paths, scope);
|
|
397
|
+
await assertNoConflict(f, baseMtime);
|
|
390
398
|
await mkdir2(dirname2(f), { recursive: true });
|
|
391
399
|
await writeFile2(f, content, "utf8");
|
|
392
400
|
}
|
|
@@ -920,17 +928,19 @@ var ConfigFileService = class {
|
|
|
920
928
|
}
|
|
921
929
|
return f;
|
|
922
930
|
}
|
|
923
|
-
//
|
|
931
|
+
// 文件不存在视为「尚未创建」,返回空串与 null mtime(与 MemoryService 一致)。
|
|
924
932
|
async read(paths, fileId, scope) {
|
|
933
|
+
const file = this.file(paths, fileId, scope);
|
|
925
934
|
try {
|
|
926
|
-
return { content: await readFile6(
|
|
935
|
+
return { content: await readFile6(file, "utf8"), mtime: await currentMtime(file) };
|
|
927
936
|
} catch (err) {
|
|
928
|
-
if (err.code === "ENOENT")
|
|
937
|
+
if (err.code === "ENOENT")
|
|
938
|
+
return { content: "", mtime: null };
|
|
929
939
|
throw err;
|
|
930
940
|
}
|
|
931
941
|
}
|
|
932
942
|
// 校验合法 JSON 后整文件写回;非法内容抛 InvalidJsonError,不触碰磁盘原文件。
|
|
933
|
-
async write(paths, fileId, scope, content) {
|
|
943
|
+
async write(paths, fileId, scope, content, baseMtime) {
|
|
934
944
|
const file = this.file(paths, fileId, scope);
|
|
935
945
|
let parsed;
|
|
936
946
|
try {
|
|
@@ -938,6 +948,7 @@ var ConfigFileService = class {
|
|
|
938
948
|
} catch {
|
|
939
949
|
throw new InvalidJsonError();
|
|
940
950
|
}
|
|
951
|
+
await assertNoConflict(file, baseMtime);
|
|
941
952
|
await this.store.writeJson(file, parsed);
|
|
942
953
|
}
|
|
943
954
|
};
|
|
@@ -989,13 +1000,14 @@ var AgentCommandService = class {
|
|
|
989
1000
|
async read(paths, kind, scope, name) {
|
|
990
1001
|
const file = this.fileFor(paths, kind, scope, name);
|
|
991
1002
|
try {
|
|
992
|
-
return { content: await readFile7(file, "utf8") };
|
|
1003
|
+
return { content: await readFile7(file, "utf8"), mtime: await currentMtime(file) };
|
|
993
1004
|
} catch {
|
|
994
1005
|
throw new Error(`\u672A\u627E\u5230 ${kind}\u300C${name}\u300D\uFF08scope: ${scope}\uFF09`);
|
|
995
1006
|
}
|
|
996
1007
|
}
|
|
997
|
-
async write(paths, kind, scope, name, content) {
|
|
1008
|
+
async write(paths, kind, scope, name, content, baseMtime) {
|
|
998
1009
|
const file = this.fileFor(paths, kind, scope, name);
|
|
1010
|
+
await assertNoConflict(file, baseMtime);
|
|
999
1011
|
await mkdir3(dirname3(file), { recursive: true });
|
|
1000
1012
|
await writeFile3(file, content, "utf8");
|
|
1001
1013
|
}
|
package/dist/server/app.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccpanel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "本地图形化管理 Claude Code 配置:MCP、Skills、子 Agent、Commands、配置文件、Memory、Sessions、Plugins",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": { "type": "git", "url": "git+https://gitee.com/jerry123456789099/ccpanel.git" },
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"bin": { "ccpanel": "dist/cli.js" },
|
|
10
10
|
"files": ["dist", "web/dist"],
|
|
11
11
|
"engines": { "node": ">=20" },
|
|
12
|
+
"publishConfig": { "registry": "https://registry.npmjs.org/" },
|
|
12
13
|
"scripts": {
|
|
13
14
|
"test": "vitest run",
|
|
14
15
|
"test:watch": "vitest",
|