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:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user