ccpanel 0.1.1 → 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 +40 -23
- 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,14 +3,17 @@ 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";
|
|
11
13
|
import { join as join6 } from "path";
|
|
12
14
|
import { createServer } from "net";
|
|
13
|
-
import { fileURLToPath
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
import { realpathSync } from "fs";
|
|
14
17
|
import { serve } from "@hono/node-server";
|
|
15
18
|
import open from "open";
|
|
16
19
|
|
|
@@ -195,17 +198,19 @@ var SkillService = class {
|
|
|
195
198
|
continue;
|
|
196
199
|
}
|
|
197
200
|
}
|
|
201
|
+
const mtime = await currentMtime(join(path, "SKILL.md")) ?? await currentMtime(path) ?? 0;
|
|
198
202
|
try {
|
|
199
203
|
const raw = await readFile(join(path, "SKILL.md"), "utf8");
|
|
200
204
|
const fm = SkillFrontmatterSchema.parse(matter(raw).data);
|
|
201
|
-
out.push({ name: e.name, description: fm.description, enabled, path, linkTarget });
|
|
205
|
+
out.push({ name: e.name, description: fm.description, enabled, path, linkTarget, mtime });
|
|
202
206
|
} catch {
|
|
203
207
|
out.push({
|
|
204
208
|
name: e.name,
|
|
205
209
|
description: "(SKILL.md \u7F3A\u5931\u6216\u65E0\u6548)",
|
|
206
210
|
enabled,
|
|
207
211
|
path,
|
|
208
|
-
linkTarget
|
|
212
|
+
linkTarget,
|
|
213
|
+
mtime
|
|
209
214
|
});
|
|
210
215
|
}
|
|
211
216
|
}
|
|
@@ -283,11 +288,12 @@ var SkillService = class {
|
|
|
283
288
|
const full = this.resolveWithin(root, relPath);
|
|
284
289
|
const buf = await readFile(full);
|
|
285
290
|
if (isBinary(buf)) throw new Error(`\u4E8C\u8FDB\u5236\u6587\u4EF6\u65E0\u6CD5\u7F16\u8F91\uFF1A${relPath}`);
|
|
286
|
-
return { content: buf.toString("utf8") };
|
|
291
|
+
return { content: buf.toString("utf8"), mtime: await currentMtime(full) };
|
|
287
292
|
}
|
|
288
|
-
async writeFileRaw(paths, scope, name, relPath, content) {
|
|
293
|
+
async writeFileRaw(paths, scope, name, relPath, content, baseMtime) {
|
|
289
294
|
const root = await this.resolveSkillDir(paths, scope, name);
|
|
290
295
|
const full = this.resolveWithin(root, relPath);
|
|
296
|
+
await assertNoConflict(full, baseMtime);
|
|
291
297
|
await mkdir(dirname(full), { recursive: true });
|
|
292
298
|
await writeFile(full, content, "utf8");
|
|
293
299
|
}
|
|
@@ -375,17 +381,20 @@ var MemoryService = class {
|
|
|
375
381
|
if (!f) throw new Error("\u7F3A\u5C11\u9879\u76EE CLAUDE.md \u8DEF\u5F84");
|
|
376
382
|
return f;
|
|
377
383
|
}
|
|
378
|
-
//
|
|
384
|
+
// 文件不存在视为「尚未创建」,返回空串与 null mtime。
|
|
379
385
|
async read(paths, scope) {
|
|
386
|
+
const f = this.file(paths, scope);
|
|
380
387
|
try {
|
|
381
|
-
return { content: await readFile2(
|
|
388
|
+
return { content: await readFile2(f, "utf8"), mtime: await currentMtime(f) };
|
|
382
389
|
} catch (err) {
|
|
383
|
-
if (err.code === "ENOENT")
|
|
390
|
+
if (err.code === "ENOENT")
|
|
391
|
+
return { content: "", mtime: null };
|
|
384
392
|
throw err;
|
|
385
393
|
}
|
|
386
394
|
}
|
|
387
|
-
async write(paths, scope, content) {
|
|
395
|
+
async write(paths, scope, content, baseMtime) {
|
|
388
396
|
const f = this.file(paths, scope);
|
|
397
|
+
await assertNoConflict(f, baseMtime);
|
|
389
398
|
await mkdir2(dirname2(f), { recursive: true });
|
|
390
399
|
await writeFile2(f, content, "utf8");
|
|
391
400
|
}
|
|
@@ -919,17 +928,19 @@ var ConfigFileService = class {
|
|
|
919
928
|
}
|
|
920
929
|
return f;
|
|
921
930
|
}
|
|
922
|
-
//
|
|
931
|
+
// 文件不存在视为「尚未创建」,返回空串与 null mtime(与 MemoryService 一致)。
|
|
923
932
|
async read(paths, fileId, scope) {
|
|
933
|
+
const file = this.file(paths, fileId, scope);
|
|
924
934
|
try {
|
|
925
|
-
return { content: await readFile6(
|
|
935
|
+
return { content: await readFile6(file, "utf8"), mtime: await currentMtime(file) };
|
|
926
936
|
} catch (err) {
|
|
927
|
-
if (err.code === "ENOENT")
|
|
937
|
+
if (err.code === "ENOENT")
|
|
938
|
+
return { content: "", mtime: null };
|
|
928
939
|
throw err;
|
|
929
940
|
}
|
|
930
941
|
}
|
|
931
942
|
// 校验合法 JSON 后整文件写回;非法内容抛 InvalidJsonError,不触碰磁盘原文件。
|
|
932
|
-
async write(paths, fileId, scope, content) {
|
|
943
|
+
async write(paths, fileId, scope, content, baseMtime) {
|
|
933
944
|
const file = this.file(paths, fileId, scope);
|
|
934
945
|
let parsed;
|
|
935
946
|
try {
|
|
@@ -937,6 +948,7 @@ var ConfigFileService = class {
|
|
|
937
948
|
} catch {
|
|
938
949
|
throw new InvalidJsonError();
|
|
939
950
|
}
|
|
951
|
+
await assertNoConflict(file, baseMtime);
|
|
940
952
|
await this.store.writeJson(file, parsed);
|
|
941
953
|
}
|
|
942
954
|
};
|
|
@@ -988,13 +1000,14 @@ var AgentCommandService = class {
|
|
|
988
1000
|
async read(paths, kind, scope, name) {
|
|
989
1001
|
const file = this.fileFor(paths, kind, scope, name);
|
|
990
1002
|
try {
|
|
991
|
-
return { content: await readFile7(file, "utf8") };
|
|
1003
|
+
return { content: await readFile7(file, "utf8"), mtime: await currentMtime(file) };
|
|
992
1004
|
} catch {
|
|
993
1005
|
throw new Error(`\u672A\u627E\u5230 ${kind}\u300C${name}\u300D\uFF08scope: ${scope}\uFF09`);
|
|
994
1006
|
}
|
|
995
1007
|
}
|
|
996
|
-
async write(paths, kind, scope, name, content) {
|
|
1008
|
+
async write(paths, kind, scope, name, content, baseMtime) {
|
|
997
1009
|
const file = this.fileFor(paths, kind, scope, name);
|
|
1010
|
+
await assertNoConflict(file, baseMtime);
|
|
998
1011
|
await mkdir3(dirname3(file), { recursive: true });
|
|
999
1012
|
await writeFile3(file, content, "utf8");
|
|
1000
1013
|
}
|
|
@@ -1047,11 +1060,8 @@ async function main() {
|
|
|
1047
1060
|
console.log(` \u5C40\u57DF\u7F51\u8BBF\u95EE\uFF1A http://${ip}:${port}`);
|
|
1048
1061
|
}
|
|
1049
1062
|
if (!process.argv.includes("--no-open")) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
} catch {
|
|
1053
|
-
console.log("\uFF08\u672A\u80FD\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740\uFF09");
|
|
1054
|
-
}
|
|
1063
|
+
open(url).then((sub) => sub?.on?.("error", () => {
|
|
1064
|
+
})).catch(() => console.log("\uFF08\u672A\u80FD\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740\uFF09"));
|
|
1055
1065
|
}
|
|
1056
1066
|
}
|
|
1057
1067
|
function lanAddresses() {
|
|
@@ -1064,7 +1074,14 @@ function lanAddresses() {
|
|
|
1064
1074
|
return out;
|
|
1065
1075
|
}
|
|
1066
1076
|
function isMainModule(argvPath, moduleUrl) {
|
|
1067
|
-
|
|
1077
|
+
if (!argvPath) return false;
|
|
1078
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
1079
|
+
if (argvPath === modulePath) return true;
|
|
1080
|
+
try {
|
|
1081
|
+
return realpathSync(argvPath) === realpathSync(modulePath);
|
|
1082
|
+
} catch {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1068
1085
|
}
|
|
1069
1086
|
if (isMainModule(process.argv[1], import.meta.url)) {
|
|
1070
1087
|
void main();
|
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",
|