feat(cc-config): manage plugins, marketplaces, hooks and settings from the dashboard

The Claude Config page was read-only for these four surfaces. It can now:

- register a marketplace from a pasted GitHub `owner/repo`, http(s) URL or
  absolute path, and install / enable / disable / update / uninstall plugins,
  all delegated to the `claude plugin …` CLI so its registry stays consistent;
- edit hooks and settings.json keys in the user, project and project-local
  layers.

Server:

- `server/lib/cc-plugin-cli.js` wraps the CLI with `execFile` and an explicit
  argv array — never a shell string. Every argument is validated before the
  spawn (shape allowlist per kind, no control characters, no leading `-`, no
  `..`), `CLAUDE_CONFIG_DIR` is pinned to the dashboard's CLAUDE_HOME, stdin is
  closed so a prompt errors instead of hanging, and mutating calls are
  serialized in-process.
- `cc-mutate.patchSettings` merges individual dotted keys into one settings
  layer. It re-reads the file unredacted, so keys the UI never displayed —
  including secret-valued ones — survive byte-for-byte. A payload carrying the
  `<redacted>` placeholder is refused (EREDACTED), as are `__proto__` /
  `prototype` / `constructor` segments; a settings file that does not parse is
  reported (EBADJSON) rather than clobbered. Backup first, then atomic write.
- Six additive routes on /api/cc-config; a non-zero CLI exit is a 422 carrying
  the CLI's own message, a missing binary a 503.

Client: install/add bars, per-card plugin and marketplace actions behind a
two-click confirm, and edit modes for hooks and settings that send only the
keys that changed. New i18n keys in en + vi; the explainer banners that said
these surfaces were read-only now describe what the writes actually do.

Tests: 61 new server tests (CLI argv construction and validation, the settings
key-merge, and the six routes against a fake `claude` on PATH).
This commit is contained in:
2026-08-25 15:58:03 +07:00
parent 03a178a718
commit 008ebf1c5c
12 changed files with 3054 additions and 118 deletions
+62 -8
View File
@@ -212,9 +212,9 @@
},
"explain": {
"hooks": {
"title": "Hooks ở đây cố tình chỉ đọc",
"body": "Hooks nằm trong khối `hooks` của các tệp `settings.json` mà Claude Code liên tục ghi vào. Sửa từ dashboard sẽ tranh chấp với CLI đang chạy và có thể ngầm phá hỏng đường tiếp nhận sự kiện của chính dashboard (`hook-handler.js` bên dưới).",
"howTo": "Tự sửa bằng một trong các cách:",
"title": "Sửa hooks tức là ghi vào settings.json",
"body": "Hooks nằm trong khối `hooks` của các tệp `settings.json` mà Claude Code liên tục ghi vào. Lưu ở đây chỉ ghi đúng sự kiện bạn sửa, sao lưu file trước, và giữ nguyên mọi khoá khác — nhưng phiên `claude` đang chạy vẫn dùng hooks cũ cho tới khi khởi động lại. Cẩn thận với các mục tiếp nhận sự kiện của chính dashboard (lệnh `hook-handler.js` bên dưới).",
"howTo": "Bạn cũng có thể sửa bằng:",
"cmd1": "claude config /hooks",
"cmd1Note": "Trình chỉnh sửa hooks tương tác trong Claude Code",
"cmd2": "$EDITOR ~/.claude/settings.json",
@@ -223,9 +223,9 @@
"cmd3Note": "Sửa thủ công khối hooks ở phạm vi dự án"
},
"settings": {
"title": "Settings ở đây cố tình chỉ đọc",
"body": "Các tệp JSON này được CLI `claude` ghi đè mỗi khi đổi model, theme, bật plugin… Việc dashboard ghi sẽ tranh chấp với CLI và mất trạng thái.",
"howTo": "Sửa settings qua chính Claude Code:",
"title": "Sửa settings là gộp theo từng khoá",
"body": "Các tệp JSON này được CLI `claude` ghi đè mỗi khi đổi model, theme, bật plugin… Lưu ở đây sẽ đọc lại file và chỉ ghi những khoá bạn đổi (sau khi sao lưu kèm mốc thời gian), nên CLI ghi song song cũng không mất gì khác — nhưng phiên đang chạy có thể chỉ nhận thay đổi sau khi khởi động lại.",
"howTo": "Bạn cũng có thể sửa bằng:",
"cmd1": "claude config set <key> <value>",
"cmd1Note": "Đặt một khoá riêng lẻ an toàn",
"cmd2": "/config",
@@ -234,8 +234,8 @@
"cmd3Note": "Sửa thủ công (chỉ khi không có process claude nào đang chạy)"
},
"plugins": {
"title": "Plugin được CLI quản lý",
"body": "Plugin là bundle có registry riêng - `installed_plugins.json` và đường dẫn cài trong `plugins/cache/`. Dashboard đọc thôi; install/uninstall giao cho `claude plugin …` để giữ registry nhất quán.",
"title": "Plugin chạy qua CLI `claude`",
"body": "Plugin là các gói có registry riêng — `installed_plugins.json` cùng đường dẫn cài trong `plugins/cache/`. Các nút ở đây chạy đúng lệnh `claude plugin …` trên máy này nên registry luôn nhất quán. Cài plugin nghĩa là chạy mã của bên thứ ba: chỉ thêm marketplace mà bạn tin tưởng.",
"install": "Cài plugin:",
"installCmd": "claude plugin install <marketplace>/<plugin>",
"uninstall": "Gỡ plugin này:",
@@ -250,10 +250,64 @@
"addCmd": "claude mcp add <name> <command> [args…]",
"remove": "Gỡ server này:",
"removeButton": "Sao chép lệnh gỡ"
},
"marketplaces": {
"title": "Marketplace là nơi plugin đến từ đó",
"body": "Marketplace là một repo git (hoặc thư mục local) có `.claude-plugin/marketplace.json` liệt kê các plugin nó cung cấp. Thêm vào tức là đăng ký trong `known_marketplaces.json` và clone về máy; plugin của nó sẽ hiện ở tab Plugins để cài. Xóa marketplace không gỡ những plugin bạn đã cài từ nó."
}
},
"snippet": {
"copy": "Sao chép",
"copied": "Đã sao chép"
},
"manage": {
"installTitle": "Cài plugin",
"installPlaceholder": "owner/repo, https://… hoặc plugin@marketplace",
"installButton": "Thêm",
"installHelp": "Dán link GitHub, URL hoặc đường dẫn local để đăng ký marketplace, hoặc gõ <plugin>@<marketplace> để cài thẳng plugin. Chạy CLI `claude plugin` trên máy này.",
"install": "Cài",
"installed": "Đã cài {{name}}.",
"availableTitle": "Có sẵn từ marketplace của bạn",
"cliUnavailable": "Không liệt kê được plugin: {{message}}",
"working": "Đang chạy…",
"enable": "Bật",
"enabled": "Đã bật {{name}}.",
"disable": "Tắt",
"disabled": "Đã tắt {{name}}.",
"update": "Cập nhật",
"updated": "Đã cập nhật {{name}}.",
"uninstall": "Gỡ",
"confirmUninstall": "Bấm lần nữa để gỡ",
"uninstalled": "Đã gỡ {{name}}.",
"remove": "Xóa",
"confirmRemove": "Bấm lần nữa để xóa",
"addMarketplaceTitle": "Thêm marketplace",
"addMarketplacePlaceholder": "owner/repo, https://github.com/… hoặc /đường/dẫn/tuyệt/đối",
"addMarketplaceButton": "Thêm",
"addMarketplaceHelp": "Đăng ký marketplace bằng `claude plugin marketplace add`; plugin của nó sẽ hiện ở tab Plugins.",
"marketplaceAdded": "Đã thêm marketplace {{name}}.",
"marketplaceUpdated": "Đã cập nhật marketplace {{name}}.",
"marketplaceRemoved": "Đã xóa marketplace {{name}}.",
"hookEvent": "Tên sự kiện (vd PreToolUse)",
"hookAddEvent": "Thêm sự kiện",
"hookRemoveEvent": "Xóa sự kiện",
"hookAddEntry": "Thêm hook",
"hookRemoveEntry": "Xóa hook",
"hookBadEvent": "“{{event}}” không phải tên sự kiện hợp lệ.",
"hookDupEvent": "Sự kiện {{event}} bị lặp hai lần.",
"hookEmptyCommand": "Mọi hook trong {{event}} đều cần có lệnh.",
"hooksSaved": "Đã lưu hook vào {{file}}.",
"hookUneditable": "Giữ nguyên (không đúng dạng sửa được): {{events}}",
"settingsKey": "Khóa",
"settingsKeyPlaceholder": "key hoặc dotted.path",
"settingsValue": "Giá trị (JSON)",
"settingsAddKey": "Thêm khóa",
"settingsRemoveKey": "Xóa khóa",
"settingsEmptyKey": "Tên khóa không được để trống.",
"settingsDupKey": "Khóa {{key}} bị lặp hai lần.",
"settingsBadJson": "Giá trị của {{key}} không phải JSON hợp lệ.",
"settingsSaved": "Đã lưu settings vào {{file}}.",
"settingsHelp": "Giá trị là JSON: \"opus\", true, 42, [\"a\"], {\"k\":1}. Chỉ những khóa bạn sửa mới được ghi; phần còn lại của file giữ nguyên.",
"settingsRedactedKeys": "Không sửa được ở đây vì giá trị đã bị che: {{keys}}"
}
}