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:
@@ -217,9 +217,9 @@
|
||||
},
|
||||
"explain": {
|
||||
"hooks": {
|
||||
"title": "Hooks are read-only here on purpose",
|
||||
"body": "Hooks live in the `hooks` block of `settings.json` files that Claude Code writes to constantly. Editing them from the dashboard would race with the live CLI and could silently break the dashboard's own ingestion (the `hook-handler.js` entry below).",
|
||||
"howTo": "Edit them yourself with one of:",
|
||||
"title": "Editing hooks writes settings.json",
|
||||
"body": "Hooks live in the `hooks` block of the `settings.json` files Claude Code writes to constantly. Edit saves only the event you changed, backs the file up first, and leaves every other key alone — but a `claude` session already running keeps its old hooks until it restarts. Take care with the dashboard`s own ingestion entries (the `hook-handler.js` commands below).",
|
||||
"howTo": "You can also edit them with:",
|
||||
"cmd1": "claude config /hooks",
|
||||
"cmd1Note": "Interactive hook editor inside Claude Code",
|
||||
"cmd2": "$EDITOR ~/.claude/settings.json",
|
||||
@@ -228,9 +228,9 @@
|
||||
"cmd3Note": "Hand-edit the project-level hooks block"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings are read-only here on purpose",
|
||||
"body": "These JSON files are rewritten by the live `claude` CLI on every model change, theme toggle, plugin enable, etc. A dashboard write would race with the CLI and lose state.",
|
||||
"howTo": "Edit settings via Claude Code itself:",
|
||||
"title": "Editing settings merges one key at a time",
|
||||
"body": "These JSON files are rewritten by the live `claude` CLI on every model change, theme toggle, plugin enable, etc. Saving here re-reads the file and writes only the keys you changed (after a timestamped backup), so a concurrent CLI write loses nothing else — but a running session may not pick the change up until it restarts.",
|
||||
"howTo": "You can also edit them with:",
|
||||
"cmd1": "claude config set <key> <value>",
|
||||
"cmd1Note": "Set an individual key safely",
|
||||
"cmd2": "/config",
|
||||
@@ -239,8 +239,8 @@
|
||||
"cmd3Note": "Hand-edit (only when no `claude` process is running)"
|
||||
},
|
||||
"plugins": {
|
||||
"title": "Plugins are managed by the CLI",
|
||||
"body": "Plugins are bundles with their own registry - `installed_plugins.json` plus install paths under `plugins/cache/`. The dashboard reads them but defers install/uninstall to `claude plugin …` so the registry stays consistent.",
|
||||
"title": "Plugins run through the `claude` CLI",
|
||||
"body": "Plugins are bundles with their own registry — `installed_plugins.json` plus install paths under `plugins/cache/`. The buttons here run the matching `claude plugin …` command on this machine, so the registry stays consistent. Installing a plugin means running third-party code: only add marketplaces you trust.",
|
||||
"install": "Install a plugin:",
|
||||
"installCmd": "claude plugin install <marketplace>/<plugin>",
|
||||
"uninstall": "Uninstall this plugin:",
|
||||
@@ -255,10 +255,64 @@
|
||||
"addCmd": "claude mcp add <name> <command> [args…]",
|
||||
"remove": "Remove this server:",
|
||||
"removeButton": "Copy remove command"
|
||||
},
|
||||
"marketplaces": {
|
||||
"title": "Marketplaces are where plugins come from",
|
||||
"body": "A marketplace is a git repo (or local folder) with a `.claude-plugin/marketplace.json` listing the plugins it offers. Adding one registers it in `known_marketplaces.json` and clones it locally; its plugins then appear on the Plugins tab, ready to install. Removing one does not uninstall plugins you already installed from it."
|
||||
}
|
||||
},
|
||||
"snippet": {
|
||||
"copy": "Copy",
|
||||
"copied": "Copied"
|
||||
},
|
||||
"manage": {
|
||||
"installTitle": "Install a plugin",
|
||||
"installPlaceholder": "owner/repo, https://… or plugin@marketplace",
|
||||
"installButton": "Add",
|
||||
"installHelp": "Paste a GitHub repo, URL or local path to register its marketplace, or type <plugin>@<marketplace> to install a plugin directly. Runs the `claude plugin` CLI on this machine.",
|
||||
"install": "Install",
|
||||
"installed": "Installed {{name}}.",
|
||||
"availableTitle": "Available from your marketplaces",
|
||||
"cliUnavailable": "Could not list available plugins: {{message}}",
|
||||
"working": "Working…",
|
||||
"enable": "Enable",
|
||||
"enabled": "Enabled {{name}}.",
|
||||
"disable": "Disable",
|
||||
"disabled": "Disabled {{name}}.",
|
||||
"update": "Update",
|
||||
"updated": "Updated {{name}}.",
|
||||
"uninstall": "Uninstall",
|
||||
"confirmUninstall": "Click again to uninstall",
|
||||
"uninstalled": "Uninstalled {{name}}.",
|
||||
"remove": "Remove",
|
||||
"confirmRemove": "Click again to remove",
|
||||
"addMarketplaceTitle": "Add a marketplace",
|
||||
"addMarketplacePlaceholder": "owner/repo, https://github.com/… or /absolute/path",
|
||||
"addMarketplaceButton": "Add",
|
||||
"addMarketplaceHelp": "Registers the marketplace with `claude plugin marketplace add`; its plugins then show up on the Plugins tab.",
|
||||
"marketplaceAdded": "Added marketplace {{name}}.",
|
||||
"marketplaceUpdated": "Updated marketplace {{name}}.",
|
||||
"marketplaceRemoved": "Removed marketplace {{name}}.",
|
||||
"hookEvent": "Event name (e.g. PreToolUse)",
|
||||
"hookAddEvent": "Add event",
|
||||
"hookRemoveEvent": "Remove event",
|
||||
"hookAddEntry": "Add hook",
|
||||
"hookRemoveEntry": "Remove hook",
|
||||
"hookBadEvent": "“{{event}}” is not a valid event name.",
|
||||
"hookDupEvent": "Event {{event}} is listed twice.",
|
||||
"hookEmptyCommand": "Every hook in {{event}} needs a command.",
|
||||
"hooksSaved": "Hooks saved to {{file}}.",
|
||||
"hookUneditable": "Left untouched (not in the editable shape): {{events}}",
|
||||
"settingsKey": "Key",
|
||||
"settingsKeyPlaceholder": "key or dotted.path",
|
||||
"settingsValue": "Value (JSON)",
|
||||
"settingsAddKey": "Add key",
|
||||
"settingsRemoveKey": "Remove key",
|
||||
"settingsEmptyKey": "A key name cannot be empty.",
|
||||
"settingsDupKey": "Key {{key}} is listed twice.",
|
||||
"settingsBadJson": "Value of {{key}} is not valid JSON.",
|
||||
"settingsSaved": "Settings saved to {{file}}.",
|
||||
"settingsHelp": "Values are JSON: \"opus\", true, 42, [\"a\"], {\"k\":1}. Only the keys you change are written; the rest of the file is left as-is.",
|
||||
"settingsRedactedKeys": "Not editable here because their values are redacted: {{keys}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1485,6 +1485,63 @@ export const api = {
|
||||
*/
|
||||
backups: (params?: { scope?: "user" | "project"; type?: CcArtifactType }) =>
|
||||
requestBackupsHelper(params),
|
||||
/**
|
||||
* GET /api/cc-config/plugins/available - every plugin offered by a
|
||||
* registered marketplace, from `claude plugin list --available --json`.
|
||||
* @returns `{ installed, available }` — the CLI's own inventory.
|
||||
*/
|
||||
pluginsAvailable: () => request<CcAvailablePlugins>("/cc-config/plugins/available"),
|
||||
/**
|
||||
* POST /api/cc-config/plugins/install - install `spec` via the `claude`
|
||||
* CLI. `scope` defaults to `"user"` server-side.
|
||||
* @returns {@link CcCliResult} — the CLI's stdout/stderr on success.
|
||||
*/
|
||||
installPlugin: (spec: string, scope?: CcCliScope) =>
|
||||
request<CcCliResult>("/cc-config/plugins/install", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ spec, scope }),
|
||||
}),
|
||||
/**
|
||||
* POST /api/cc-config/plugins/action - enable/disable/update/uninstall an
|
||||
* installed plugin by its `<name>@<marketplace>` key.
|
||||
*/
|
||||
pluginAction: (key: string, action: CcPluginAction) =>
|
||||
request<CcCliResult>("/cc-config/plugins/action", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ key, action }),
|
||||
}),
|
||||
/**
|
||||
* POST /api/cc-config/marketplaces/add - register a marketplace from a
|
||||
* GitHub `owner/repo`, an http(s) URL, or an absolute local path.
|
||||
*/
|
||||
addMarketplace: (source: string, scope?: CcCliScope) =>
|
||||
request<CcCliResult>("/cc-config/marketplaces/add", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ source, scope }),
|
||||
}),
|
||||
/** POST /api/cc-config/marketplaces/action - update or remove a marketplace. */
|
||||
marketplaceAction: (name: string, action: CcMarketplaceAction) =>
|
||||
request<CcCliResult>("/cc-config/marketplaces/action", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name, action }),
|
||||
}),
|
||||
/**
|
||||
* PATCH /api/cc-config/settings - merge individual keys into one
|
||||
* settings.json layer (this is also how hooks are edited, as
|
||||
* `set: { "hooks.<Event>": [...] }` in settings.json's nested shape).
|
||||
*
|
||||
* Only the named keys are touched: the server re-reads the file
|
||||
* unredacted, so keys the UI never displayed survive untouched. A value
|
||||
* containing the `<redacted>` placeholder is rejected.
|
||||
*
|
||||
* @param args Target `scope` plus dotted-path `set` and/or `unset`.
|
||||
* @returns {@link CcSettingsPatchResult} — path, backup path, changed keys.
|
||||
*/
|
||||
patchSettings: (args: CcSettingsPatchArgs) =>
|
||||
request<CcSettingsPatchResult>("/cc-config/settings", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(args),
|
||||
}),
|
||||
},
|
||||
|
||||
// ────────────────────────────────── Run API ─────────────────────────────────
|
||||
@@ -2301,6 +2358,59 @@ export interface CcHookSource {
|
||||
hooks: Record<string, CcHookEntry[]>;
|
||||
}
|
||||
|
||||
/** Install scope understood by the `claude plugin` CLI. Note `"local"`, which
|
||||
* is the same layer {@link CcSettingsSource} calls `"project-local"`. */
|
||||
export type CcCliScope = "user" | "project" | "local";
|
||||
|
||||
/** What POST /api/cc-config/plugins/action can do to an installed plugin. */
|
||||
export type CcPluginAction = "enable" | "disable" | "update" | "uninstall";
|
||||
|
||||
/** What POST /api/cc-config/marketplaces/action can do to a marketplace. */
|
||||
export type CcMarketplaceAction = "update" | "remove";
|
||||
|
||||
/** Result of a `claude plugin …` run the dashboard triggered. A non-zero CLI
|
||||
* exit arrives as an HTTP 422 error instead of one of these. */
|
||||
export interface CcCliResult {
|
||||
ok: true;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
/** One entry of `claude plugin list --available --json`'s `available` array. */
|
||||
export interface CcAvailablePlugin {
|
||||
/** `<name>@<marketplace>` — the exact spec to pass to `installPlugin`. */
|
||||
pluginId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
marketplaceName?: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
/** GET /api/cc-config/plugins/available - the CLI's own plugin inventory. */
|
||||
export interface CcAvailablePlugins {
|
||||
installed: { id?: string; version?: string; enabled?: boolean }[];
|
||||
available: CcAvailablePlugin[];
|
||||
}
|
||||
|
||||
/** Body of PATCH /api/cc-config/settings - a per-key merge, never a whole-file
|
||||
* overwrite. Keys are dotted paths (`"permissions.allow"`, `"hooks.Stop"`). */
|
||||
export interface CcSettingsPatchArgs {
|
||||
scope: "user" | "project" | "project-local";
|
||||
set?: Record<string, unknown>;
|
||||
unset?: string[];
|
||||
}
|
||||
|
||||
/** Result of a settings key-merge. */
|
||||
export interface CcSettingsPatchResult {
|
||||
/** Absolute path of the settings file that was written. */
|
||||
path: string;
|
||||
/** Pre-write backup, or null when the file did not exist yet. */
|
||||
backupPath: string | null;
|
||||
created: boolean;
|
||||
/** Keys actually changed - an `unset` of an absent key is not listed. */
|
||||
changed: string[];
|
||||
}
|
||||
|
||||
/** One settings.json layer's raw contents, from GET /api/cc-config/settings. */
|
||||
export interface CcSettingsSource {
|
||||
scope: "user" | "project" | "project-local";
|
||||
|
||||
+966
-90
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user