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.
@@ -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,8 +3,10 @@ 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";
@@ -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(this.file(paths, scope), "utf8") };
388
+ return { content: await readFile2(f, "utf8"), mtime: await currentMtime(f) };
383
389
  } catch (err) {
384
- if (err.code === "ENOENT") return { content: "" };
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
- // 文件不存在视为「尚未创建」,返回空串(与 MemoryService 一致)。
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(this.file(paths, fileId, scope), "utf8") };
935
+ return { content: await readFile6(file, "utf8"), mtime: await currentMtime(file) };
927
936
  } catch (err) {
928
- if (err.code === "ENOENT") return { content: "" };
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
  }
@@ -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.2",
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",