mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
support plugins in external agent config migration (#17855)
This commit is contained in:
committed by
GitHub
Unverified
parent
2bfa627613
commit
ca650561d6
@@ -605,6 +605,16 @@
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MigrationDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"itemType": {
|
||||
"$ref": "#/definitions/ExternalAgentConfigMigrationItemType"
|
||||
}
|
||||
@@ -620,6 +630,7 @@
|
||||
"AGENTS_MD",
|
||||
"CONFIG",
|
||||
"SKILLS",
|
||||
"PLUGINS",
|
||||
"MCP_SERVER_CONFIG"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -1359,6 +1370,20 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MigrationDetails": {
|
||||
"properties": {
|
||||
"plugins": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginsMigration"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plugins"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModeKind": {
|
||||
"description": "Initial collaboration mode to use when the TUI starts.",
|
||||
"enum": [
|
||||
@@ -1478,6 +1503,24 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginsMigration": {
|
||||
"properties": {
|
||||
"marketplaceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginNames": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaceName",
|
||||
"pluginNames"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ReadOnlyAccess": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
@@ -7509,6 +7509,16 @@
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/MigrationDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"itemType": {
|
||||
"$ref": "#/definitions/v2/ExternalAgentConfigMigrationItemType"
|
||||
}
|
||||
@@ -7524,6 +7534,7 @@
|
||||
"AGENTS_MD",
|
||||
"CONFIG",
|
||||
"SKILLS",
|
||||
"PLUGINS",
|
||||
"MCP_SERVER_CONFIG"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -9551,6 +9562,20 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MigrationDetails": {
|
||||
"properties": {
|
||||
"plugins": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PluginsMigration"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plugins"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModeKind": {
|
||||
"description": "Initial collaboration mode to use when the TUI starts.",
|
||||
"enum": [
|
||||
@@ -10473,6 +10498,24 @@
|
||||
"title": "PluginUninstallResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginsMigration": {
|
||||
"properties": {
|
||||
"marketplaceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginNames": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaceName",
|
||||
"pluginNames"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ProfileV2": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
|
||||
@@ -4126,6 +4126,16 @@
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MigrationDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"itemType": {
|
||||
"$ref": "#/definitions/ExternalAgentConfigMigrationItemType"
|
||||
}
|
||||
@@ -4141,6 +4151,7 @@
|
||||
"AGENTS_MD",
|
||||
"CONFIG",
|
||||
"SKILLS",
|
||||
"PLUGINS",
|
||||
"MCP_SERVER_CONFIG"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -6323,6 +6334,20 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"MigrationDetails": {
|
||||
"properties": {
|
||||
"plugins": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginsMigration"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plugins"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ModeKind": {
|
||||
"description": "Initial collaboration mode to use when the TUI starts.",
|
||||
"enum": [
|
||||
@@ -7245,6 +7270,24 @@
|
||||
"title": "PluginUninstallResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PluginsMigration": {
|
||||
"properties": {
|
||||
"marketplaceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginNames": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaceName",
|
||||
"pluginNames"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ProfileV2": {
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MigrationDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"itemType": {
|
||||
"$ref": "#/definitions/ExternalAgentConfigMigrationItemType"
|
||||
}
|
||||
@@ -28,9 +38,42 @@
|
||||
"AGENTS_MD",
|
||||
"CONFIG",
|
||||
"SKILLS",
|
||||
"PLUGINS",
|
||||
"MCP_SERVER_CONFIG"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MigrationDetails": {
|
||||
"properties": {
|
||||
"plugins": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginsMigration"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plugins"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginsMigration": {
|
||||
"properties": {
|
||||
"marketplaceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginNames": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaceName",
|
||||
"pluginNames"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MigrationDetails"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"itemType": {
|
||||
"$ref": "#/definitions/ExternalAgentConfigMigrationItemType"
|
||||
}
|
||||
@@ -28,9 +38,42 @@
|
||||
"AGENTS_MD",
|
||||
"CONFIG",
|
||||
"SKILLS",
|
||||
"PLUGINS",
|
||||
"MCP_SERVER_CONFIG"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MigrationDetails": {
|
||||
"properties": {
|
||||
"plugins": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PluginsMigration"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"plugins"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PluginsMigration": {
|
||||
"properties": {
|
||||
"marketplaceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginNames": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"marketplaceName",
|
||||
"pluginNames"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
|
||||
+2
-1
@@ -2,9 +2,10 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ExternalAgentConfigMigrationItemType } from "./ExternalAgentConfigMigrationItemType";
|
||||
import type { MigrationDetails } from "./MigrationDetails";
|
||||
|
||||
export type ExternalAgentConfigMigrationItem = { itemType: ExternalAgentConfigMigrationItemType, description: string,
|
||||
/**
|
||||
* Null or empty means home-scoped migration; non-empty means repo-scoped migration.
|
||||
*/
|
||||
cwd: string | null, };
|
||||
cwd: string | null, details: MigrationDetails | null, };
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ExternalAgentConfigMigrationItemType = "AGENTS_MD" | "CONFIG" | "SKILLS" | "MCP_SERVER_CONFIG";
|
||||
export type ExternalAgentConfigMigrationItemType = "AGENTS_MD" | "CONFIG" | "SKILLS" | "PLUGINS" | "MCP_SERVER_CONFIG";
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PluginsMigration } from "./PluginsMigration";
|
||||
|
||||
export type MigrationDetails = { plugins: Array<PluginsMigration>, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PluginsMigration = { marketplaceName: string, pluginNames: Array<string>, };
|
||||
@@ -198,6 +198,7 @@ export type { McpToolCallStatus } from "./McpToolCallStatus";
|
||||
export type { MemoryCitation } from "./MemoryCitation";
|
||||
export type { MemoryCitationEntry } from "./MemoryCitationEntry";
|
||||
export type { MergeStrategy } from "./MergeStrategy";
|
||||
export type { MigrationDetails } from "./MigrationDetails";
|
||||
export type { Model } from "./Model";
|
||||
export type { ModelAvailabilityNux } from "./ModelAvailabilityNux";
|
||||
export type { ModelListParams } from "./ModelListParams";
|
||||
@@ -236,6 +237,7 @@ export type { PluginSource } from "./PluginSource";
|
||||
export type { PluginSummary } from "./PluginSummary";
|
||||
export type { PluginUninstallParams } from "./PluginUninstallParams";
|
||||
export type { PluginUninstallResponse } from "./PluginUninstallResponse";
|
||||
export type { PluginsMigration } from "./PluginsMigration";
|
||||
export type { ProfileV2 } from "./ProfileV2";
|
||||
export type { RateLimitSnapshot } from "./RateLimitSnapshot";
|
||||
export type { RateLimitWindow } from "./RateLimitWindow";
|
||||
|
||||
@@ -944,11 +944,33 @@ pub enum ExternalAgentConfigMigrationItemType {
|
||||
#[serde(rename = "SKILLS")]
|
||||
#[ts(rename = "SKILLS")]
|
||||
Skills,
|
||||
#[serde(rename = "PLUGINS")]
|
||||
#[ts(rename = "PLUGINS")]
|
||||
Plugins,
|
||||
#[serde(rename = "MCP_SERVER_CONFIG")]
|
||||
#[ts(rename = "MCP_SERVER_CONFIG")]
|
||||
McpServerConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PluginsMigration {
|
||||
#[serde(rename = "marketplaceName")]
|
||||
#[ts(rename = "marketplaceName")]
|
||||
pub marketplace_name: String,
|
||||
#[serde(rename = "pluginNames")]
|
||||
#[ts(rename = "pluginNames")]
|
||||
pub plugin_names: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct MigrationDetails {
|
||||
pub plugins: Vec<PluginsMigration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -957,6 +979,7 @@ pub struct ExternalAgentConfigMigrationItem {
|
||||
pub description: String,
|
||||
/// Null or empty means home-scoped migration; non-empty means repo-scoped migration.
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub details: Option<MigrationDetails>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
@@ -6592,6 +6615,39 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_agent_config_plugins_details_round_trip() {
|
||||
let item: ExternalAgentConfigMigrationItem = serde_json::from_value(json!({
|
||||
"itemType": "PLUGINS",
|
||||
"description": "Install supported plugins from Claude settings",
|
||||
"cwd": absolute_path_string("repo"),
|
||||
"details": {
|
||||
"plugins": [
|
||||
{
|
||||
"marketplaceName": "team-marketplace",
|
||||
"pluginNames": ["asana"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}))
|
||||
.expect("plugins migration item should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
item,
|
||||
ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
||||
description: "Install supported plugins from Claude settings".to_string(),
|
||||
cwd: Some(PathBuf::from(absolute_path_string("repo"))),
|
||||
details: Some(MigrationDetails {
|
||||
plugins: vec![PluginsMigration {
|
||||
marketplace_name: "team-marketplace".to_string(),
|
||||
plugin_names: vec!["asana".to_string()],
|
||||
}],
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_execution_request_approval_rejects_relative_additional_permission_paths() {
|
||||
let err = serde_json::from_value::<CommandExecutionRequestApprovalParams>(json!({
|
||||
|
||||
@@ -196,8 +196,8 @@ Example with notification opt-out:
|
||||
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.
|
||||
- `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id.
|
||||
- `config/read` — fetch the effective config on disk after resolving config layering.
|
||||
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome` and optional `cwds`; each detected item includes `cwd` (`null` for home).
|
||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home).
|
||||
- `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome` and optional `cwds`; each detected item includes `cwd` (`null` for home), and plugin migration items may additionally include structured `details` grouping plugin ids under each detected marketplace name.
|
||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin `details` returned by detect.
|
||||
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
|
||||
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads.
|
||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), pinned feature values (`featureRequirements`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly`.
|
||||
|
||||
@@ -6,6 +6,8 @@ use codex_app_server_protocol::ExternalAgentConfigImportResponse;
|
||||
use codex_app_server_protocol::ExternalAgentConfigMigrationItem;
|
||||
use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::MigrationDetails;
|
||||
use codex_app_server_protocol::PluginsMigration;
|
||||
use codex_core::external_agent_config::ExternalAgentConfigDetectOptions;
|
||||
use codex_core::external_agent_config::ExternalAgentConfigMigrationItem as CoreMigrationItem;
|
||||
use codex_core::external_agent_config::ExternalAgentConfigMigrationItemType as CoreMigrationItemType;
|
||||
@@ -51,12 +53,25 @@ impl ExternalAgentConfigApi {
|
||||
CoreMigrationItemType::AgentsMd => {
|
||||
ExternalAgentConfigMigrationItemType::AgentsMd
|
||||
}
|
||||
CoreMigrationItemType::Plugins => {
|
||||
ExternalAgentConfigMigrationItemType::Plugins
|
||||
}
|
||||
CoreMigrationItemType::McpServerConfig => {
|
||||
ExternalAgentConfigMigrationItemType::McpServerConfig
|
||||
}
|
||||
},
|
||||
description: migration_item.description,
|
||||
cwd: migration_item.cwd,
|
||||
details: migration_item.details.map(|details| MigrationDetails {
|
||||
plugins: details
|
||||
.plugins
|
||||
.into_iter()
|
||||
.map(|plugin| PluginsMigration {
|
||||
marketplace_name: plugin.marketplace_name,
|
||||
plugin_names: plugin.plugin_names,
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
@@ -82,15 +97,33 @@ impl ExternalAgentConfigApi {
|
||||
ExternalAgentConfigMigrationItemType::AgentsMd => {
|
||||
CoreMigrationItemType::AgentsMd
|
||||
}
|
||||
ExternalAgentConfigMigrationItemType::Plugins => {
|
||||
CoreMigrationItemType::Plugins
|
||||
}
|
||||
ExternalAgentConfigMigrationItemType::McpServerConfig => {
|
||||
CoreMigrationItemType::McpServerConfig
|
||||
}
|
||||
},
|
||||
description: migration_item.description,
|
||||
cwd: migration_item.cwd,
|
||||
details: migration_item.details.map(|details| {
|
||||
codex_core::external_agent_config::MigrationDetails {
|
||||
plugins: details
|
||||
.plugins
|
||||
.into_iter()
|
||||
.map(|plugin| {
|
||||
codex_core::external_agent_config::PluginsMigration {
|
||||
marketplace_name: plugin.marketplace_name,
|
||||
plugin_names: plugin.plugin_names,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_io_error)?;
|
||||
|
||||
Ok(ExternalAgentConfigImportResponse {})
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
use crate::plugins::MarketplaceAddRequest;
|
||||
use crate::plugins::PluginId;
|
||||
use crate::plugins::PluginInstallRequest;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::plugins::add_marketplace;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
@@ -9,6 +16,8 @@ use toml::Value as TomlValue;
|
||||
|
||||
const EXTERNAL_AGENT_CONFIG_DETECT_METRIC: &str = "codex.external_agent_config.detect";
|
||||
const EXTERNAL_AGENT_CONFIG_IMPORT_METRIC: &str = "codex.external_agent_config.import";
|
||||
// Installed marketplace roots always expose their manifest at this relative path.
|
||||
const INSTALLED_MARKETPLACE_MANIFEST_RELATIVE_PATH: &str = ".agents/plugins/marketplace.json";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExternalAgentConfigDetectOptions {
|
||||
@@ -21,36 +30,57 @@ pub enum ExternalAgentConfigMigrationItemType {
|
||||
Config,
|
||||
Skills,
|
||||
AgentsMd,
|
||||
Plugins,
|
||||
McpServerConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PluginsMigration {
|
||||
pub marketplace_name: String,
|
||||
pub plugin_names: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MigrationDetails {
|
||||
pub plugins: Vec<PluginsMigration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
struct PluginImportOutcome {
|
||||
succeeded_marketplaces: Vec<String>,
|
||||
succeeded_plugin_ids: Vec<String>,
|
||||
failed_marketplaces: Vec<String>,
|
||||
failed_plugin_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExternalAgentConfigMigrationItem {
|
||||
pub item_type: ExternalAgentConfigMigrationItemType,
|
||||
pub description: String,
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub details: Option<MigrationDetails>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExternalAgentConfigService {
|
||||
codex_home: PathBuf,
|
||||
claude_home: PathBuf,
|
||||
external_agent_home: PathBuf,
|
||||
}
|
||||
|
||||
impl ExternalAgentConfigService {
|
||||
pub fn new(codex_home: PathBuf) -> Self {
|
||||
let claude_home = default_claude_home();
|
||||
let external_agent_home = default_external_agent_home();
|
||||
Self {
|
||||
codex_home,
|
||||
claude_home,
|
||||
external_agent_home,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn new_for_test(codex_home: PathBuf, claude_home: PathBuf) -> Self {
|
||||
fn new_for_test(codex_home: PathBuf, external_agent_home: PathBuf) -> Self {
|
||||
Self {
|
||||
codex_home,
|
||||
claude_home,
|
||||
external_agent_home,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +103,10 @@ impl ExternalAgentConfigService {
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub fn import(&self, migration_items: Vec<ExternalAgentConfigMigrationItem>) -> io::Result<()> {
|
||||
pub async fn import(
|
||||
&self,
|
||||
migration_items: Vec<ExternalAgentConfigMigrationItem>,
|
||||
) -> io::Result<()> {
|
||||
for migration_item in migration_items {
|
||||
match migration_item.item_type {
|
||||
ExternalAgentConfigMigrationItemType::Config => {
|
||||
@@ -100,6 +133,35 @@ impl ExternalAgentConfigService {
|
||||
/*skills_count*/ None,
|
||||
);
|
||||
}
|
||||
ExternalAgentConfigMigrationItemType::Plugins => {
|
||||
let service = self.clone();
|
||||
let cwd = migration_item.cwd;
|
||||
let details = migration_item.details;
|
||||
tokio::spawn(async move {
|
||||
match service.import_plugins(cwd.as_deref(), details).await {
|
||||
Ok(outcome) => {
|
||||
tracing::info!(
|
||||
succeeded_marketplaces = outcome.succeeded_marketplaces.len(),
|
||||
succeeded_plugin_ids = outcome.succeeded_plugin_ids.len(),
|
||||
failed_marketplaces = outcome.failed_marketplaces.len(),
|
||||
failed_plugin_ids = outcome.failed_plugin_ids.len(),
|
||||
"external agent config plugin import completed"
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"external agent config plugin import failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
emit_migration_metric(
|
||||
EXTERNAL_AGENT_CONFIG_IMPORT_METRIC,
|
||||
ExternalAgentConfigMigrationItemType::Plugins,
|
||||
/*skills_count*/ None,
|
||||
);
|
||||
}
|
||||
ExternalAgentConfigMigrationItemType::McpServerConfig => {}
|
||||
}
|
||||
}
|
||||
@@ -114,17 +176,15 @@ impl ExternalAgentConfigService {
|
||||
) -> io::Result<()> {
|
||||
let cwd = repo_root.map(Path::to_path_buf);
|
||||
let source_settings = repo_root.map_or_else(
|
||||
|| self.claude_home.join("settings.json"),
|
||||
|| self.external_agent_home.join("settings.json"),
|
||||
|repo_root| repo_root.join(".claude").join("settings.json"),
|
||||
);
|
||||
let settings = read_external_settings(&source_settings)?;
|
||||
let target_config = repo_root.map_or_else(
|
||||
|| self.codex_home.join("config.toml"),
|
||||
|repo_root| repo_root.join(".codex").join("config.toml"),
|
||||
);
|
||||
if source_settings.is_file() {
|
||||
let raw_settings = fs::read_to_string(&source_settings)?;
|
||||
let settings: JsonValue = serde_json::from_str(&raw_settings)
|
||||
.map_err(|err| invalid_data_error(err.to_string()))?;
|
||||
if let Some(settings) = settings.as_ref() {
|
||||
let migrated = build_config_from_external(&settings)?;
|
||||
if !is_empty_toml_table(&migrated) {
|
||||
let mut should_include = true;
|
||||
@@ -149,6 +209,7 @@ impl ExternalAgentConfigService {
|
||||
target_config.display()
|
||||
),
|
||||
cwd: cwd.clone(),
|
||||
details: None,
|
||||
});
|
||||
emit_migration_metric(
|
||||
EXTERNAL_AGENT_CONFIG_DETECT_METRIC,
|
||||
@@ -159,8 +220,15 @@ impl ExternalAgentConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
self.detect_plugin_migration(
|
||||
source_settings.as_path(),
|
||||
cwd.clone(),
|
||||
settings.as_ref(),
|
||||
items,
|
||||
);
|
||||
|
||||
let source_skills = repo_root.map_or_else(
|
||||
|| self.claude_home.join("skills"),
|
||||
|| self.external_agent_home.join("skills"),
|
||||
|repo_root| repo_root.join(".claude").join("skills"),
|
||||
);
|
||||
let target_skills = repo_root.map_or_else(
|
||||
@@ -177,6 +245,7 @@ impl ExternalAgentConfigService {
|
||||
target_skills.display()
|
||||
),
|
||||
cwd: cwd.clone(),
|
||||
details: None,
|
||||
});
|
||||
emit_migration_metric(
|
||||
EXTERNAL_AGENT_CONFIG_DETECT_METRIC,
|
||||
@@ -188,7 +257,7 @@ impl ExternalAgentConfigService {
|
||||
let source_agents_md = if let Some(repo_root) = repo_root {
|
||||
find_repo_agents_md_source(repo_root)?
|
||||
} else {
|
||||
let path = self.claude_home.join("CLAUDE.md");
|
||||
let path = self.external_agent_home.join("CLAUDE.md");
|
||||
is_non_empty_text_file(&path)?.then_some(path)
|
||||
};
|
||||
let target_agents_md = repo_root.map_or_else(
|
||||
@@ -206,6 +275,7 @@ impl ExternalAgentConfigService {
|
||||
target_agents_md.display()
|
||||
),
|
||||
cwd,
|
||||
details: None,
|
||||
});
|
||||
emit_migration_metric(
|
||||
EXTERNAL_AGENT_CONFIG_DETECT_METRIC,
|
||||
@@ -224,6 +294,109 @@ impl ExternalAgentConfigService {
|
||||
.unwrap_or_else(|| PathBuf::from(".agents").join("skills"))
|
||||
}
|
||||
|
||||
fn detect_plugin_migration(
|
||||
&self,
|
||||
source_settings: &Path,
|
||||
cwd: Option<PathBuf>,
|
||||
settings: Option<&JsonValue>,
|
||||
items: &mut Vec<ExternalAgentConfigMigrationItem>,
|
||||
) {
|
||||
let Some(plugin_details) = settings.and_then(extract_plugin_migration_details) else {
|
||||
return;
|
||||
};
|
||||
|
||||
items.push(ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
||||
description: format!("Import enabled plugins from {}", source_settings.display()),
|
||||
cwd,
|
||||
details: Some(plugin_details),
|
||||
});
|
||||
emit_migration_metric(
|
||||
EXTERNAL_AGENT_CONFIG_DETECT_METRIC,
|
||||
ExternalAgentConfigMigrationItemType::Plugins,
|
||||
/*skills_count*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
async fn import_plugins(
|
||||
&self,
|
||||
cwd: Option<&Path>,
|
||||
details: Option<MigrationDetails>,
|
||||
) -> io::Result<PluginImportOutcome> {
|
||||
let Some(MigrationDetails { plugins }) = details else {
|
||||
return Err(invalid_data_error(
|
||||
"plugins migration item is missing details".to_string(),
|
||||
));
|
||||
};
|
||||
let mut outcome = PluginImportOutcome::default();
|
||||
let plugins_manager = PluginsManager::new(self.codex_home.clone());
|
||||
for plugin_group in plugins {
|
||||
let marketplace_name = plugin_group.marketplace_name.clone();
|
||||
let plugin_names = plugin_group.plugin_names;
|
||||
let plugin_ids = plugin_names
|
||||
.iter()
|
||||
.map(|plugin_name| format!("{plugin_name}@{marketplace_name}"))
|
||||
.collect::<Vec<_>>();
|
||||
let import_source =
|
||||
read_marketplace_import_source(cwd, &self.external_agent_home, &marketplace_name)?;
|
||||
let Some(import_source) = import_source else {
|
||||
outcome.failed_marketplaces.push(marketplace_name);
|
||||
outcome.failed_plugin_ids.extend(plugin_ids);
|
||||
continue;
|
||||
};
|
||||
let request = MarketplaceAddRequest {
|
||||
source: import_source.source,
|
||||
ref_name: import_source.ref_name,
|
||||
sparse_paths: Vec::new(),
|
||||
};
|
||||
let add_marketplace_outcome = add_marketplace(self.codex_home.clone(), request).await;
|
||||
let marketplace_path = match add_marketplace_outcome {
|
||||
Ok(add_marketplace_outcome) => {
|
||||
match AbsolutePathBuf::try_from(
|
||||
add_marketplace_outcome
|
||||
.installed_root
|
||||
.join(INSTALLED_MARKETPLACE_MANIFEST_RELATIVE_PATH),
|
||||
) {
|
||||
Ok(path) => {
|
||||
outcome
|
||||
.succeeded_marketplaces
|
||||
.push(marketplace_name.clone());
|
||||
path
|
||||
}
|
||||
Err(_) => {
|
||||
outcome.failed_marketplaces.push(marketplace_name);
|
||||
outcome.failed_plugin_ids.extend(plugin_ids);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
outcome.failed_marketplaces.push(marketplace_name);
|
||||
outcome.failed_plugin_ids.extend(plugin_ids);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for plugin_name in plugin_names {
|
||||
match plugins_manager
|
||||
.install_plugin(PluginInstallRequest {
|
||||
plugin_name: plugin_name.clone(),
|
||||
marketplace_path: marketplace_path.clone(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => outcome
|
||||
.succeeded_plugin_ids
|
||||
.push(format!("{plugin_name}@{marketplace_name}")),
|
||||
Err(_) => outcome
|
||||
.failed_plugin_ids
|
||||
.push(format!("{plugin_name}@{marketplace_name}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
fn import_config(&self, cwd: Option<&Path>) -> io::Result<()> {
|
||||
let (source_settings, target_config) = if let Some(repo_root) = find_repo_root(cwd)? {
|
||||
(
|
||||
@@ -234,7 +407,7 @@ impl ExternalAgentConfigService {
|
||||
return Ok(());
|
||||
} else {
|
||||
(
|
||||
self.claude_home.join("settings.json"),
|
||||
self.external_agent_home.join("settings.json"),
|
||||
self.codex_home.join("config.toml"),
|
||||
)
|
||||
};
|
||||
@@ -286,7 +459,7 @@ impl ExternalAgentConfigService {
|
||||
return Ok(0);
|
||||
} else {
|
||||
(
|
||||
self.claude_home.join("skills"),
|
||||
self.external_agent_home.join("skills"),
|
||||
self.home_target_skills_dir(),
|
||||
)
|
||||
};
|
||||
@@ -326,7 +499,7 @@ impl ExternalAgentConfigService {
|
||||
return Ok(());
|
||||
} else {
|
||||
(
|
||||
self.claude_home.join("CLAUDE.md"),
|
||||
self.external_agent_home.join("CLAUDE.md"),
|
||||
self.codex_home.join("AGENTS.md"),
|
||||
)
|
||||
};
|
||||
@@ -345,7 +518,7 @@ impl ExternalAgentConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_claude_home() -> PathBuf {
|
||||
fn default_external_agent_home() -> PathBuf {
|
||||
if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
|
||||
return PathBuf::from(home).join(".claude");
|
||||
}
|
||||
@@ -353,6 +526,139 @@ fn default_claude_home() -> PathBuf {
|
||||
PathBuf::from(".claude")
|
||||
}
|
||||
|
||||
fn read_external_settings(path: &Path) -> io::Result<Option<JsonValue>> {
|
||||
if !path.is_file() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let raw_settings = fs::read_to_string(path)?;
|
||||
let settings =
|
||||
serde_json::from_str(&raw_settings).map_err(|err| invalid_data_error(err.to_string()))?;
|
||||
Ok(Some(settings))
|
||||
}
|
||||
|
||||
fn extract_plugin_migration_details(settings: &JsonValue) -> Option<MigrationDetails> {
|
||||
let mut plugins = BTreeMap::new();
|
||||
for plugin_id in collect_enabled_plugins(settings) {
|
||||
let Ok(plugin_id) = PluginId::parse(&plugin_id) else {
|
||||
continue;
|
||||
};
|
||||
let plugin_group = plugins
|
||||
.entry(plugin_id.marketplace_name.clone())
|
||||
.or_insert_with(|| PluginsMigration {
|
||||
marketplace_name: plugin_id.marketplace_name.clone(),
|
||||
plugin_names: Vec::new(),
|
||||
});
|
||||
plugin_group.plugin_names.push(plugin_id.plugin_name);
|
||||
}
|
||||
|
||||
let plugins = plugins
|
||||
.into_values()
|
||||
.filter_map(|mut plugin_group| {
|
||||
if plugin_group.plugin_names.is_empty() {
|
||||
return None;
|
||||
}
|
||||
plugin_group.plugin_names.sort();
|
||||
Some(plugin_group)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if plugins.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(MigrationDetails { plugins })
|
||||
}
|
||||
|
||||
fn collect_enabled_plugins(settings: &JsonValue) -> Vec<String> {
|
||||
let Some(enabled_plugins) = settings
|
||||
.as_object()
|
||||
.and_then(|settings| settings.get("enabledPlugins"))
|
||||
.and_then(JsonValue::as_object)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
enabled_plugins
|
||||
.iter()
|
||||
.filter_map(|(plugin_key, enabled)| {
|
||||
if !enabled.as_bool().unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
PluginId::parse(plugin_key)
|
||||
.ok()
|
||||
.map(|plugin_id| plugin_id.as_key())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_marketplace_import_sources(
|
||||
settings: &JsonValue,
|
||||
) -> BTreeMap<String, MarketplaceImportSource> {
|
||||
let Some(extra_known_marketplaces) = settings
|
||||
.as_object()
|
||||
.and_then(|settings| settings.get("extraKnownMarketplaces"))
|
||||
.and_then(JsonValue::as_object)
|
||||
else {
|
||||
return BTreeMap::new();
|
||||
};
|
||||
|
||||
extra_known_marketplaces
|
||||
.iter()
|
||||
.filter_map(|(name, value)| {
|
||||
let source_fields = if let Some(source) = value.get("source")
|
||||
&& source.is_object()
|
||||
{
|
||||
source.as_object()?
|
||||
} else {
|
||||
value.as_object()?
|
||||
};
|
||||
let source = source_fields
|
||||
.get("repo")
|
||||
.or_else(|| source_fields.get("url"))
|
||||
.or_else(|| source_fields.get("path"))
|
||||
.or_else(|| value.get("source"))?
|
||||
.as_str()?
|
||||
.trim()
|
||||
.to_string();
|
||||
if source.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ref_name = source_fields
|
||||
.get("ref")
|
||||
.or_else(|| value.get("ref"))
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
Some((name.clone(), MarketplaceImportSource { source, ref_name }))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct MarketplaceImportSource {
|
||||
source: String,
|
||||
ref_name: Option<String>,
|
||||
}
|
||||
|
||||
fn read_marketplace_import_source(
|
||||
cwd: Option<&Path>,
|
||||
external_agent_home: &Path,
|
||||
marketplace_name: &str,
|
||||
) -> io::Result<Option<MarketplaceImportSource>> {
|
||||
let source_settings = cwd.map_or_else(
|
||||
|| external_agent_home.join("settings.json"),
|
||||
|cwd| cwd.join(".claude").join("settings.json"),
|
||||
);
|
||||
let Some(settings) = read_external_settings(&source_settings)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(collect_marketplace_import_sources(&settings).remove(marketplace_name))
|
||||
}
|
||||
|
||||
fn find_repo_root(cwd: Option<&Path>) -> io::Result<Option<PathBuf>> {
|
||||
let Some(cwd) = cwd.filter(|cwd| !cwd.as_os_str().is_empty()) else {
|
||||
return Ok(None);
|
||||
@@ -480,11 +786,11 @@ fn is_skill_md(path: &Path) -> bool {
|
||||
|
||||
fn rewrite_and_copy_text_file(source: &Path, target: &Path) -> io::Result<()> {
|
||||
let source_contents = fs::read_to_string(source)?;
|
||||
let rewritten = rewrite_claude_terms(&source_contents);
|
||||
let rewritten = rewrite_external_agent_terms(&source_contents);
|
||||
fs::write(target, rewritten)
|
||||
}
|
||||
|
||||
fn rewrite_claude_terms(content: &str) -> String {
|
||||
fn rewrite_external_agent_terms(content: &str) -> String {
|
||||
let mut rewritten = replace_case_insensitive_with_boundaries(content, "claude.md", "AGENTS.md");
|
||||
for from in [
|
||||
"claude code",
|
||||
@@ -662,6 +968,7 @@ fn migration_metric_tags(
|
||||
ExternalAgentConfigMigrationItemType::Config => "config",
|
||||
ExternalAgentConfigMigrationItemType::Skills => "skills",
|
||||
ExternalAgentConfigMigrationItemType::AgentsMd => "agents_md",
|
||||
ExternalAgentConfigMigrationItemType::Plugins => "plugins",
|
||||
ExternalAgentConfigMigrationItemType::McpServerConfig => "mcp_server_config",
|
||||
};
|
||||
let mut tags = vec![("migration_type", migration_type.to_string())];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::io;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn fixture_paths() -> (TempDir, PathBuf, PathBuf) {
|
||||
@@ -13,6 +14,15 @@ fn service_for_paths(claude_home: PathBuf, codex_home: PathBuf) -> ExternalAgent
|
||||
ExternalAgentConfigService::new_for_test(codex_home, claude_home)
|
||||
}
|
||||
|
||||
fn github_plugin_details() -> MigrationDetails {
|
||||
MigrationDetails {
|
||||
plugins: vec![PluginsMigration {
|
||||
marketplace_name: "acme-tools".to_string(),
|
||||
plugin_names: vec!["formatter".to_string()],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_home_lists_config_skills_and_agents_md() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
@@ -44,6 +54,7 @@ fn detect_home_lists_config_skills_and_agents_md() {
|
||||
codex_home.join("config.toml").display()
|
||||
),
|
||||
cwd: None,
|
||||
details: None,
|
||||
},
|
||||
ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Skills,
|
||||
@@ -53,6 +64,7 @@ fn detect_home_lists_config_skills_and_agents_md() {
|
||||
agents_skills.display()
|
||||
),
|
||||
cwd: None,
|
||||
details: None,
|
||||
},
|
||||
ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
||||
@@ -62,6 +74,7 @@ fn detect_home_lists_config_skills_and_agents_md() {
|
||||
codex_home.join("AGENTS.md").display()
|
||||
),
|
||||
cwd: None,
|
||||
details: None,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -93,6 +106,7 @@ fn detect_repo_lists_agents_md_for_each_cwd() {
|
||||
repo_root.join("AGENTS.md").display(),
|
||||
),
|
||||
cwd: Some(repo_root.clone()),
|
||||
details: None,
|
||||
},
|
||||
ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
||||
@@ -102,14 +116,15 @@ fn detect_repo_lists_agents_md_for_each_cwd() {
|
||||
repo_root.join("AGENTS.md").display(),
|
||||
),
|
||||
cwd: Some(repo_root),
|
||||
details: None,
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(items, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_home_migrates_supported_config_fields_skills_and_agents_md() {
|
||||
#[tokio::test]
|
||||
async fn import_home_migrates_supported_config_fields_skills_and_agents_md() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
let agents_skills = codex_home
|
||||
.parent()
|
||||
@@ -134,18 +149,22 @@ fn import_home_migrates_supported_config_fields_skills_and_agents_md() {
|
||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
||||
description: String::new(),
|
||||
cwd: None,
|
||||
details: None,
|
||||
},
|
||||
ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Config,
|
||||
description: String::new(),
|
||||
cwd: None,
|
||||
details: None,
|
||||
},
|
||||
ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Skills,
|
||||
description: String::new(),
|
||||
cwd: None,
|
||||
details: None,
|
||||
},
|
||||
])
|
||||
.await
|
||||
.expect("import");
|
||||
|
||||
assert_eq!(
|
||||
@@ -164,8 +183,8 @@ fn import_home_migrates_supported_config_fields_skills_and_agents_md() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_home_skips_empty_config_migration() {
|
||||
#[tokio::test]
|
||||
async fn import_home_skips_empty_config_migration() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&claude_home).expect("create claude home");
|
||||
fs::write(
|
||||
@@ -179,7 +198,9 @@ fn import_home_skips_empty_config_migration() {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Config,
|
||||
description: String::new(),
|
||||
cwd: None,
|
||||
details: None,
|
||||
}])
|
||||
.await
|
||||
.expect("import");
|
||||
|
||||
assert!(!codex_home.join("config.toml").exists());
|
||||
@@ -239,8 +260,8 @@ fn detect_home_skips_skills_when_all_skill_directories_exist() {
|
||||
assert_eq!(items, Vec::<ExternalAgentConfigMigrationItem>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_repo_agents_md_rewrites_terms_and_skips_non_empty_targets() {
|
||||
#[tokio::test]
|
||||
async fn import_repo_agents_md_rewrites_terms_and_skips_non_empty_targets() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let repo_root = root.path().join("repo-a");
|
||||
let repo_with_existing_target = root.path().join("repo-b");
|
||||
@@ -264,13 +285,16 @@ fn import_repo_agents_md_rewrites_terms_and_skips_non_empty_targets() {
|
||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
||||
description: String::new(),
|
||||
cwd: Some(repo_root.clone()),
|
||||
details: None,
|
||||
},
|
||||
ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
||||
description: String::new(),
|
||||
cwd: Some(repo_with_existing_target.clone()),
|
||||
details: None,
|
||||
},
|
||||
])
|
||||
.await
|
||||
.expect("import");
|
||||
|
||||
assert_eq!(
|
||||
@@ -284,8 +308,8 @@ fn import_repo_agents_md_rewrites_terms_and_skips_non_empty_targets() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_repo_agents_md_overwrites_empty_targets() {
|
||||
#[tokio::test]
|
||||
async fn import_repo_agents_md_overwrites_empty_targets() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let repo_root = root.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".git")).expect("create git");
|
||||
@@ -297,7 +321,9 @@ fn import_repo_agents_md_overwrites_empty_targets() {
|
||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
||||
description: String::new(),
|
||||
cwd: Some(repo_root.clone()),
|
||||
details: None,
|
||||
}])
|
||||
.await
|
||||
.expect("import");
|
||||
|
||||
assert_eq!(
|
||||
@@ -336,12 +362,13 @@ fn detect_repo_prefers_non_empty_dot_claude_agents_source() {
|
||||
repo_root.join("AGENTS.md").display(),
|
||||
),
|
||||
cwd: Some(repo_root),
|
||||
details: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_repo_uses_non_empty_dot_claude_agents_source() {
|
||||
#[tokio::test]
|
||||
async fn import_repo_uses_non_empty_dot_claude_agents_source() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let repo_root = root.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".git")).expect("create git");
|
||||
@@ -358,7 +385,9 @@ fn import_repo_uses_non_empty_dot_claude_agents_source() {
|
||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
||||
description: String::new(),
|
||||
cwd: Some(repo_root.clone()),
|
||||
details: None,
|
||||
}])
|
||||
.await
|
||||
.expect("import");
|
||||
|
||||
assert_eq!(
|
||||
@@ -378,6 +407,141 @@ fn migration_metric_tags_for_skills_include_skills_count() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_home_lists_enabled_plugins_from_settings() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&claude_home).expect("create claude home");
|
||||
fs::write(
|
||||
claude_home.join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true,
|
||||
"deployer@acme-tools": true,
|
||||
"analyzer@security-plugins": false
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
|
||||
let items = service_for_paths(claude_home.clone(), codex_home)
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: true,
|
||||
cwds: None,
|
||||
})
|
||||
.expect("detect");
|
||||
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
||||
description: format!(
|
||||
"Import enabled plugins from {}",
|
||||
claude_home.join("settings.json").display()
|
||||
),
|
||||
cwd: None,
|
||||
details: Some(MigrationDetails {
|
||||
plugins: vec![PluginsMigration {
|
||||
marketplace_name: "acme-tools".to_string(),
|
||||
plugin_names: vec!["deployer".to_string(), "formatter".to_string()],
|
||||
}],
|
||||
}),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_plugins_requires_details() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
|
||||
let err = service_for_paths(claude_home, codex_home)
|
||||
.import_plugins(/*cwd*/ None, /*details*/ None)
|
||||
.await
|
||||
.expect_err("expected missing details error");
|
||||
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
|
||||
assert_eq!(err.to_string(), "plugins migration item is missing details");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_plugins_requires_source_marketplace_details() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&claude_home).expect("create claude home");
|
||||
fs::write(
|
||||
claude_home.join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"acme-tools": {
|
||||
"source": "github",
|
||||
"repo": "acme-corp/claude-plugins"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
|
||||
let outcome = service_for_paths(claude_home, codex_home)
|
||||
.import_plugins(
|
||||
/*cwd*/ None,
|
||||
Some(MigrationDetails {
|
||||
plugins: vec![PluginsMigration {
|
||||
marketplace_name: "other-tools".to_string(),
|
||||
plugin_names: github_plugin_details().plugins[0].plugin_names.clone(),
|
||||
}],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("import plugins");
|
||||
|
||||
assert_eq!(
|
||||
outcome,
|
||||
PluginImportOutcome {
|
||||
succeeded_marketplaces: Vec::new(),
|
||||
succeeded_plugin_ids: Vec::new(),
|
||||
failed_marketplaces: vec!["other-tools".to_string()],
|
||||
failed_plugin_ids: vec!["formatter@other-tools".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_plugins_defers_marketplace_source_validation_to_add_marketplace() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&claude_home).expect("create claude home");
|
||||
fs::write(
|
||||
claude_home.join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"acme-tools": {
|
||||
"source": "local",
|
||||
"path": "./external_plugins/acme-tools"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
|
||||
let outcome = service_for_paths(claude_home, codex_home)
|
||||
.import_plugins(/*cwd*/ None, Some(github_plugin_details()))
|
||||
.await
|
||||
.expect("import plugins");
|
||||
|
||||
assert_eq!(
|
||||
outcome,
|
||||
PluginImportOutcome {
|
||||
succeeded_marketplaces: Vec::new(),
|
||||
succeeded_plugin_ids: Vec::new(),
|
||||
failed_marketplaces: vec!["acme-tools".to_string()],
|
||||
failed_plugin_ids: vec!["formatter@acme-tools".to_string()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_skills_returns_only_new_skill_directory_count() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
|
||||
Reference in New Issue
Block a user