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.
@@ -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 SkillFileWriteSchema = z.object({ content: z.string() });
114
- var MemoryWriteSchema = z.object({ content: z.string() });
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({ content: z.string() });
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({ content: z7.string() });
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
- createApp
7
- } from "./chunk-7ROXP6AO.js";
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, pathToFileURL } from "url";
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(this.file(paths, scope), "utf8") };
388
+ return { content: await readFile2(f, "utf8"), mtime: await currentMtime(f) };
382
389
  } catch (err) {
383
- if (err.code === "ENOENT") return { content: "" };
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
- // 文件不存在视为「尚未创建」,返回空串(与 MemoryService 一致)。
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(this.file(paths, fileId, scope), "utf8") };
935
+ return { content: await readFile6(file, "utf8"), mtime: await currentMtime(file) };
926
936
  } catch (err) {
927
- if (err.code === "ENOENT") return { content: "" };
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
- try {
1051
- await open(url);
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
- return !!argvPath && moduleUrl === pathToFileURL(argvPath).href;
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();
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "../chunk-7ROXP6AO.js";
3
+ } from "../chunk-SZFZF7CH.js";
4
4
  export {
5
5
  createApp
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccpanel",
3
- "version": "0.1.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",