diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index d2808a626..db2065f75 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1721,6 +1721,17 @@ ], "type": "object" }, + "MarketplaceUpgradeParams": { + "properties": { + "marketplaceName": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, "McpResourceReadParams": { "properties": { "server": { @@ -4904,6 +4915,30 @@ "title": "Marketplace/removeRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "marketplace/upgrade" + ], + "title": "Marketplace/upgradeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/MarketplaceUpgradeParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Marketplace/upgradeRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index c87a704c0..6904ef716 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -689,6 +689,30 @@ "title": "Marketplace/removeRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "marketplace/upgrade" + ], + "title": "Marketplace/upgradeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/MarketplaceUpgradeParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Marketplace/upgradeRequest", + "type": "object" + }, { "properties": { "id": { @@ -10318,6 +10342,64 @@ "title": "MarketplaceRemoveResponse", "type": "object" }, + "MarketplaceUpgradeErrorInfo": { + "properties": { + "marketplaceName": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "marketplaceName", + "message" + ], + "type": "object" + }, + "MarketplaceUpgradeParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "marketplaceName": { + "type": [ + "string", + "null" + ] + } + }, + "title": "MarketplaceUpgradeParams", + "type": "object" + }, + "MarketplaceUpgradeResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "errors": { + "items": { + "$ref": "#/definitions/v2/MarketplaceUpgradeErrorInfo" + }, + "type": "array" + }, + "selectedMarketplaces": { + "items": { + "type": "string" + }, + "type": "array" + }, + "upgradedRoots": { + "items": { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "errors", + "selectedMarketplaces", + "upgradedRoots" + ], + "title": "MarketplaceUpgradeResponse", + "type": "object" + }, "McpAuthStatus": { "enum": [ "unsupported", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 9f8b3d586..bc00828e6 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -1380,6 +1380,30 @@ "title": "Marketplace/removeRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "marketplace/upgrade" + ], + "title": "Marketplace/upgradeRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/MarketplaceUpgradeParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Marketplace/upgradeRequest", + "type": "object" + }, { "properties": { "id": { @@ -7033,6 +7057,64 @@ "title": "MarketplaceRemoveResponse", "type": "object" }, + "MarketplaceUpgradeErrorInfo": { + "properties": { + "marketplaceName": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "marketplaceName", + "message" + ], + "type": "object" + }, + "MarketplaceUpgradeParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "marketplaceName": { + "type": [ + "string", + "null" + ] + } + }, + "title": "MarketplaceUpgradeParams", + "type": "object" + }, + "MarketplaceUpgradeResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "errors": { + "items": { + "$ref": "#/definitions/MarketplaceUpgradeErrorInfo" + }, + "type": "array" + }, + "selectedMarketplaces": { + "items": { + "type": "string" + }, + "type": "array" + }, + "upgradedRoots": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "errors", + "selectedMarketplaces", + "upgradedRoots" + ], + "title": "MarketplaceUpgradeResponse", + "type": "object" + }, "McpAuthStatus": { "enum": [ "unsupported", diff --git a/codex-rs/app-server-protocol/schema/json/v2/MarketplaceUpgradeParams.json b/codex-rs/app-server-protocol/schema/json/v2/MarketplaceUpgradeParams.json new file mode 100644 index 000000000..684d134cb --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/MarketplaceUpgradeParams.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "marketplaceName": { + "type": [ + "string", + "null" + ] + } + }, + "title": "MarketplaceUpgradeParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/MarketplaceUpgradeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/MarketplaceUpgradeResponse.json new file mode 100644 index 000000000..678824163 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/MarketplaceUpgradeResponse.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + }, + "MarketplaceUpgradeErrorInfo": { + "properties": { + "marketplaceName": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "marketplaceName", + "message" + ], + "type": "object" + } + }, + "properties": { + "errors": { + "items": { + "$ref": "#/definitions/MarketplaceUpgradeErrorInfo" + }, + "type": "array" + }, + "selectedMarketplaces": { + "items": { + "type": "string" + }, + "type": "array" + }, + "upgradedRoots": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "errors", + "selectedMarketplaces", + "upgradedRoots" + ], + "title": "MarketplaceUpgradeResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 3a65075b6..7aaa17461 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -38,6 +38,7 @@ import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams"; import type { LoginAccountParams } from "./v2/LoginAccountParams"; import type { MarketplaceAddParams } from "./v2/MarketplaceAddParams"; import type { MarketplaceRemoveParams } from "./v2/MarketplaceRemoveParams"; +import type { MarketplaceUpgradeParams } from "./v2/MarketplaceUpgradeParams"; import type { McpResourceReadParams } from "./v2/McpResourceReadParams"; import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams"; import type { McpServerToolCallParams } from "./v2/McpServerToolCallParams"; @@ -75,4 +76,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "device/key/create", id: RequestId, params: DeviceKeyCreateParams, } | { "method": "device/key/public", id: RequestId, params: DeviceKeyPublicParams, } | { "method": "device/key/sign", id: RequestId, params: DeviceKeySignParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "device/key/create", id: RequestId, params: DeviceKeyCreateParams, } | { "method": "device/key/public", id: RequestId, params: DeviceKeyPublicParams, } | { "method": "device/key/sign", id: RequestId, params: DeviceKeySignParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceUpgradeErrorInfo.ts b/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceUpgradeErrorInfo.ts new file mode 100644 index 000000000..d54f8f592 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceUpgradeErrorInfo.ts @@ -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 MarketplaceUpgradeErrorInfo = { marketplaceName: string, message: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceUpgradeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceUpgradeParams.ts new file mode 100644 index 000000000..6d2e5f50b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceUpgradeParams.ts @@ -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 MarketplaceUpgradeParams = { marketplaceName?: string | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceUpgradeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceUpgradeResponse.ts new file mode 100644 index 000000000..456fbdcca --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/MarketplaceUpgradeResponse.ts @@ -0,0 +1,7 @@ +// 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 { AbsolutePathBuf } from "../AbsolutePathBuf"; +import type { MarketplaceUpgradeErrorInfo } from "./MarketplaceUpgradeErrorInfo"; + +export type MarketplaceUpgradeResponse = { selectedMarketplaces: Array, upgradedRoots: Array, errors: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index b58dc0248..be747508a 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -178,6 +178,9 @@ export type { MarketplaceInterface } from "./MarketplaceInterface"; export type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo"; export type { MarketplaceRemoveParams } from "./MarketplaceRemoveParams"; export type { MarketplaceRemoveResponse } from "./MarketplaceRemoveResponse"; +export type { MarketplaceUpgradeErrorInfo } from "./MarketplaceUpgradeErrorInfo"; +export type { MarketplaceUpgradeParams } from "./MarketplaceUpgradeParams"; +export type { MarketplaceUpgradeResponse } from "./MarketplaceUpgradeResponse"; export type { McpAuthStatus } from "./McpAuthStatus"; export type { McpElicitationArrayType } from "./McpElicitationArrayType"; export type { McpElicitationBooleanSchema } from "./McpElicitationBooleanSchema"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 3ca969584..9fa6d98e7 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -357,6 +357,10 @@ client_request_definitions! { params: v2::MarketplaceRemoveParams, response: v2::MarketplaceRemoveResponse, }, + MarketplaceUpgrade => "marketplace/upgrade" { + params: v2::MarketplaceUpgradeParams, + response: v2::MarketplaceUpgradeResponse, + }, PluginList => "plugin/list" { params: v2::PluginListParams, response: v2::PluginListResponse, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 4ec916fd9..b6726679f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -4104,6 +4104,31 @@ pub struct MarketplaceRemoveResponse { pub installed_root: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceUpgradeParams { + #[ts(optional = nullable)] + pub marketplace_name: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceUpgradeResponse { + pub selected_marketplaces: Vec, + pub upgraded_roots: Vec, + pub errors: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct MarketplaceUpgradeErrorInfo { + pub marketplace_name: String, + pub message: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -9790,6 +9815,36 @@ mod tests { ); } + #[test] + fn marketplace_upgrade_params_serialization_uses_optional_marketplace_name() { + assert_eq!( + serde_json::to_value(MarketplaceUpgradeParams { + marketplace_name: None, + }) + .unwrap(), + json!({ + "marketplaceName": null, + }), + ); + + assert_eq!( + serde_json::from_value::(json!({})).unwrap(), + MarketplaceUpgradeParams { + marketplace_name: None, + }, + ); + + assert_eq!( + serde_json::to_value(MarketplaceUpgradeParams { + marketplace_name: Some("debug".to_string()), + }) + .unwrap(), + json!({ + "marketplaceName": "debug", + }), + ); + } + #[test] fn plugin_marketplace_entry_serializes_remote_only_path_as_null() { assert_eq!( @@ -10036,6 +10091,37 @@ mod tests { ); } + #[test] + fn marketplace_upgrade_response_serializes_camel_case_fields() { + let upgraded_root = if cfg!(windows) { + r"C:\marketplaces\debug" + } else { + "/tmp/marketplaces/debug" + }; + let upgraded_root = AbsolutePathBuf::try_from(PathBuf::from(upgraded_root)).unwrap(); + let upgraded_root_json = upgraded_root.as_path().display().to_string(); + + assert_eq!( + serde_json::to_value(MarketplaceUpgradeResponse { + selected_marketplaces: vec!["debug".to_string()], + upgraded_roots: vec![upgraded_root], + errors: vec![MarketplaceUpgradeErrorInfo { + marketplace_name: "broken".to_string(), + message: "failed to clone".to_string(), + }], + }) + .unwrap(), + json!({ + "selectedMarketplaces": ["debug"], + "upgradedRoots": [upgraded_root_json], + "errors": [{ + "marketplaceName": "broken", + "message": "failed to clone", + }], + }), + ); + } + #[test] fn codex_error_info_serializes_http_status_code_in_camel_case() { let value = CodexErrorInfo::ResponseTooManyFailedAttempts { diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c6eeac587..e46d785b3 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -192,6 +192,7 @@ Example with notification opt-out: - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). - `marketplace/add` — add a remote plugin marketplace from an HTTP(S) Git URL, SSH Git URL, or GitHub `owner/repo` shorthand, then persist it into the user marketplace config. Returns the installed root path plus whether the marketplace was already present. - `marketplace/remove` — remove a configured marketplace by name from the user marketplace config, and delete its installed marketplace root when one exists. +- `marketplace/upgrade` — upgrade all configured Git plugin marketplaces, or one named marketplace when `marketplaceName` is provided. Returns selected marketplace names, upgraded roots, and per-marketplace errors. - `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**). - `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**). - `skills/changed` — notification emitted when watched local skill files change. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index a5f2eb400..ae7514a9c 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -88,6 +88,9 @@ use codex_app_server_protocol::MarketplaceAddResponse; use codex_app_server_protocol::MarketplaceInterface; use codex_app_server_protocol::MarketplaceRemoveParams; use codex_app_server_protocol::MarketplaceRemoveResponse; +use codex_app_server_protocol::MarketplaceUpgradeErrorInfo; +use codex_app_server_protocol::MarketplaceUpgradeParams; +use codex_app_server_protocol::MarketplaceUpgradeResponse; use codex_app_server_protocol::McpResourceReadParams; use codex_app_server_protocol::McpResourceReadResponse; use codex_app_server_protocol::McpServerOauthLoginCompletedNotification; @@ -980,6 +983,10 @@ impl CodexMessageProcessor { self.marketplace_remove(to_connection_request_id(request_id), params) .await; } + ClientRequest::MarketplaceUpgrade { request_id, params } => { + self.marketplace_upgrade(to_connection_request_id(request_id), params) + .await; + } ClientRequest::PluginList { request_id, params } => { self.plugin_list(to_connection_request_id(request_id), params) .await; @@ -6776,6 +6783,61 @@ impl CodexMessageProcessor { } } } + + async fn marketplace_upgrade( + &self, + request_id: ConnectionRequestId, + params: MarketplaceUpgradeParams, + ) { + let config = match self.load_latest_config(/*fallback_cwd*/ None).await { + Ok(config) => config, + Err(err) => { + self.outgoing.send_error(request_id, err).await; + return; + } + }; + let plugins_manager = self.thread_manager.plugins_manager(); + let MarketplaceUpgradeParams { marketplace_name } = params; + + let result = tokio::task::spawn_blocking(move || { + plugins_manager + .upgrade_configured_marketplaces_for_config(&config, marketplace_name.as_deref()) + }) + .await; + + match result { + Ok(Ok(outcome)) => { + self.outgoing + .send_response( + request_id, + MarketplaceUpgradeResponse { + selected_marketplaces: outcome.selected_marketplaces, + upgraded_roots: outcome.upgraded_roots, + errors: outcome + .errors + .into_iter() + .map(|err| MarketplaceUpgradeErrorInfo { + marketplace_name: err.marketplace_name, + message: err.message, + }) + .collect(), + }, + ) + .await; + } + Ok(Err(message)) => { + self.send_invalid_request_error(request_id, message).await; + } + Err(err) => { + self.send_internal_error( + request_id, + format!("failed to upgrade marketplaces: {err}"), + ) + .await; + } + } + } + async fn marketplace_add(&self, request_id: ConnectionRequestId, params: MarketplaceAddParams) { let result = add_marketplace_to_codex_home( self.config.codex_home.to_path_buf(), diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 2279939ec..befa248e8 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -49,6 +49,7 @@ use codex_app_server_protocol::ListMcpServerStatusParams; use codex_app_server_protocol::LoginAccountParams; use codex_app_server_protocol::MarketplaceAddParams; use codex_app_server_protocol::MarketplaceRemoveParams; +use codex_app_server_protocol::MarketplaceUpgradeParams; use codex_app_server_protocol::McpResourceReadParams; use codex_app_server_protocol::McpServerToolCallParams; use codex_app_server_protocol::MockExperimentalMethodParams; @@ -565,6 +566,15 @@ impl McpProcess { self.send_request("marketplace/remove", params).await } + /// Send a `marketplace/upgrade` JSON-RPC request. + pub async fn send_marketplace_upgrade_request( + &mut self, + params: MarketplaceUpgradeParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("marketplace/upgrade", params).await + } + /// Send a `plugin/install` JSON-RPC request. pub async fn send_plugin_install_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/v2/marketplace_upgrade.rs b/codex-rs/app-server/tests/suite/v2/marketplace_upgrade.rs new file mode 100644 index 000000000..c10bb5cae --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/marketplace_upgrade.rs @@ -0,0 +1,303 @@ +use std::path::Path; +use std::process::Command; +use std::time::Duration; + +use anyhow::Context; +use anyhow::Result; +use app_test_support::McpProcess; +use app_test_support::to_response; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::MarketplaceUpgradeParams; +use codex_app_server_protocol::MarketplaceUpgradeResponse; +use codex_app_server_protocol::RequestId; +use codex_config::MarketplaceConfigUpdate; +use codex_config::record_user_marketplace; +use codex_utils_absolute_path::AbsolutePathBuf; +use pretty_assertions::assert_eq; +use tempfile::TempDir; +use tokio::time::timeout; + +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); +const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/marketplaces"; + +fn run_git(cwd: &Path, args: &[&str]) -> Result { + let output = Command::new("git").current_dir(cwd).args(args).output()?; + if !output.status.success() { + anyhow::bail!( + "git {} failed in {}: {}", + args.join(" "), + cwd.display(), + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +fn write_marketplace_files(root: &Path, marketplace_name: &str, marker: &str) -> Result<()> { + std::fs::create_dir_all(root.join(".agents/plugins"))?; + std::fs::write( + root.join(".agents/plugins/marketplace.json"), + format!(r#"{{"name":"{marketplace_name}","plugins":[]}}"#), + )?; + std::fs::write(root.join("marker.txt"), marker)?; + Ok(()) +} + +fn init_marketplace_repo(root: &Path, marketplace_name: &str, marker: &str) -> Result { + run_git(root, &["init"])?; + run_git(root, &["config", "user.email", "codex@example.com"])?; + run_git(root, &["config", "user.name", "Codex Tests"])?; + write_marketplace_files(root, marketplace_name, marker)?; + run_git(root, &["add", "."])?; + run_git(root, &["commit", "-m", "initial marketplace"])?; + run_git(root, &["rev-parse", "HEAD"]) +} + +fn commit_marketplace_marker(root: &Path, marker: &str) -> Result { + std::fs::write(root.join("marker.txt"), marker)?; + run_git(root, &["add", "marker.txt"])?; + run_git(root, &["commit", "-m", "update marker"])?; + run_git(root, &["rev-parse", "HEAD"]) +} + +fn configured_git_marketplace_update<'a>( + source: &'a str, + last_revision: Option<&'a str>, +) -> MarketplaceConfigUpdate<'a> { + MarketplaceConfigUpdate { + last_updated: "2026-04-13T00:00:00Z", + last_revision, + source_type: "git", + source, + ref_name: None, + sparse_paths: &[], + } +} + +fn configured_local_marketplace_update(source: &str) -> MarketplaceConfigUpdate<'_> { + MarketplaceConfigUpdate { + last_updated: "2026-04-13T00:00:00Z", + last_revision: None, + source_type: "local", + source, + ref_name: None, + sparse_paths: &[], + } +} + +fn record_git_marketplace( + codex_home: &Path, + marketplace_name: &str, + source: &Path, + last_revision: &str, +) -> Result<()> { + let source = source.display().to_string(); + record_user_marketplace( + codex_home, + marketplace_name, + &configured_git_marketplace_update(&source, Some(last_revision)), + )?; + Ok(()) +} + +fn disable_plugin_startup_tasks(codex_home: &Path) -> Result<()> { + let config_path = codex_home.join("config.toml"); + let config = std::fs::read_to_string(&config_path)?; + std::fs::write( + config_path, + format!("{config}\n[features]\nplugins = false\n"), + )?; + Ok(()) +} + +fn marketplace_install_root(codex_home: &Path) -> std::path::PathBuf { + codex_home.join(INSTALLED_MARKETPLACES_DIR) +} + +fn expected_installed_root(codex_home: &Path, marketplace_name: &str) -> Result { + AbsolutePathBuf::try_from( + marketplace_install_root(&codex_home.canonicalize()?).join(marketplace_name), + ) + .context("expected installed root should be absolute") +} + +async fn send_marketplace_upgrade( + mcp: &mut McpProcess, + marketplace_name: Option<&str>, +) -> Result { + let request_id = mcp + .send_marketplace_upgrade_request(MarketplaceUpgradeParams { + marketplace_name: marketplace_name.map(str::to_string), + }) + .await?; + + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + to_response(response) +} + +#[tokio::test] +async fn marketplace_upgrade_all_configured_git_marketplaces() -> Result<()> { + let codex_home = TempDir::new()?; + let debug_source = TempDir::new()?; + let tools_source = TempDir::new()?; + let debug_old_revision = init_marketplace_repo(debug_source.path(), "debug", "debug old")?; + let tools_old_revision = init_marketplace_repo(tools_source.path(), "tools", "tools old")?; + let debug_new_revision = commit_marketplace_marker(debug_source.path(), "debug new")?; + let tools_new_revision = commit_marketplace_marker(tools_source.path(), "tools new")?; + record_git_marketplace( + codex_home.path(), + "debug", + debug_source.path(), + &debug_old_revision, + )?; + record_git_marketplace( + codex_home.path(), + "tools", + tools_source.path(), + &tools_old_revision, + )?; + disable_plugin_startup_tasks(codex_home.path())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let debug_root = expected_installed_root(codex_home.path(), "debug")?; + let tools_root = expected_installed_root(codex_home.path(), "tools")?; + let response = send_marketplace_upgrade(&mut mcp, /*marketplace_name*/ None).await?; + + assert_eq!( + response, + MarketplaceUpgradeResponse { + selected_marketplaces: vec!["debug".to_string(), "tools".to_string()], + upgraded_roots: vec![debug_root.clone(), tools_root.clone()], + errors: Vec::new(), + } + ); + assert_eq!( + std::fs::read_to_string(debug_root.as_path().join("marker.txt"))?, + "debug new" + ); + assert_eq!( + std::fs::read_to_string(tools_root.as_path().join("marker.txt"))?, + "tools new" + ); + let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; + assert!(config.contains(&debug_new_revision)); + assert!(config.contains(&tools_new_revision)); + Ok(()) +} + +#[tokio::test] +async fn marketplace_upgrade_named_marketplace_only() -> Result<()> { + let codex_home = TempDir::new()?; + let debug_source = TempDir::new()?; + let tools_source = TempDir::new()?; + let debug_old_revision = init_marketplace_repo(debug_source.path(), "debug", "debug old")?; + let tools_old_revision = init_marketplace_repo(tools_source.path(), "tools", "tools old")?; + commit_marketplace_marker(debug_source.path(), "debug new")?; + commit_marketplace_marker(tools_source.path(), "tools new")?; + record_git_marketplace( + codex_home.path(), + "debug", + debug_source.path(), + &debug_old_revision, + )?; + record_git_marketplace( + codex_home.path(), + "tools", + tools_source.path(), + &tools_old_revision, + )?; + disable_plugin_startup_tasks(codex_home.path())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let tools_root = expected_installed_root(codex_home.path(), "tools")?; + let response = send_marketplace_upgrade(&mut mcp, Some("tools")).await?; + + assert_eq!( + response, + MarketplaceUpgradeResponse { + selected_marketplaces: vec!["tools".to_string()], + upgraded_roots: vec![tools_root.clone()], + errors: Vec::new(), + } + ); + assert_eq!( + std::fs::read_to_string(tools_root.as_path().join("marker.txt"))?, + "tools new" + ); + assert!( + !marketplace_install_root(codex_home.path()) + .join("debug") + .exists() + ); + Ok(()) +} + +#[tokio::test] +async fn marketplace_upgrade_returns_empty_roots_when_already_up_to_date() -> Result<()> { + let codex_home = TempDir::new()?; + let source = TempDir::new()?; + let old_revision = init_marketplace_repo(source.path(), "debug", "debug old")?; + commit_marketplace_marker(source.path(), "debug new")?; + record_git_marketplace(codex_home.path(), "debug", source.path(), &old_revision)?; + disable_plugin_startup_tasks(codex_home.path())?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + let first_response = send_marketplace_upgrade(&mut mcp, Some("debug")).await?; + assert!(first_response.errors.is_empty()); + + let response = send_marketplace_upgrade(&mut mcp, Some("debug")).await?; + + assert_eq!( + response, + MarketplaceUpgradeResponse { + selected_marketplaces: vec!["debug".to_string()], + upgraded_roots: Vec::new(), + errors: Vec::new(), + } + ); + Ok(()) +} + +#[tokio::test] +async fn marketplace_upgrade_rejects_unknown_or_non_git_marketplace() -> Result<()> { + let codex_home = TempDir::new()?; + let local_source = TempDir::new()?; + record_user_marketplace( + codex_home.path(), + "local-only", + &configured_local_marketplace_update(&local_source.path().display().to_string()), + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + for marketplace_name in ["missing", "local-only"] { + let request_id = mcp + .send_marketplace_upgrade_request(MarketplaceUpgradeParams { + marketplace_name: Some(marketplace_name.to_string()), + }) + .await?; + + let err = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!(err.error.code, -32600); + assert_eq!( + err.error.message, + format!("marketplace `{marketplace_name}` is not configured as a Git marketplace"), + ); + } + Ok(()) +} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index 4ab21ffc5..4a3f23183 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -19,6 +19,7 @@ mod fs; mod initialize; mod marketplace_add; mod marketplace_remove; +mod marketplace_upgrade; mod mcp_resource; mod mcp_server_elicitation; mod mcp_server_status; diff --git a/codex-rs/core-plugins/src/marketplace_upgrade/git.rs b/codex-rs/core-plugins/src/marketplace_upgrade/git.rs index 80a7c68f6..9a46465db 100644 --- a/codex-rs/core-plugins/src/marketplace_upgrade/git.rs +++ b/codex-rs/core-plugins/src/marketplace_upgrade/git.rs @@ -1,4 +1,5 @@ use std::path::Path; +use std::path::PathBuf; use std::process::Command; use std::process::Output; use std::process::Stdio; @@ -46,9 +47,10 @@ pub(super) fn clone_git_source( destination: &Path, timeout: Duration, ) -> Result { + let git_destination = git_path_arg(destination); if sparse_paths.is_empty() { let output = run_git_command_with_timeout( - git_command().arg("clone").arg(source).arg(destination), + git_command().arg("clone").arg(source).arg(&git_destination), "git clone marketplace source", timeout, )?; @@ -57,7 +59,7 @@ pub(super) fn clone_git_source( let output = run_git_command_with_timeout( git_command() .arg("-C") - .arg(destination) + .arg(&git_destination) .arg("checkout") .arg(ref_name), "git checkout marketplace ref", @@ -65,7 +67,7 @@ pub(super) fn clone_git_source( )?; ensure_git_success(&output, "git checkout marketplace ref")?; } - return git_worktree_revision(destination, timeout); + return git_worktree_revision(&git_destination, timeout); } let output = run_git_command_with_timeout( @@ -74,7 +76,7 @@ pub(super) fn clone_git_source( .arg("--filter=blob:none") .arg("--no-checkout") .arg(source) - .arg(destination), + .arg(&git_destination), "git clone marketplace source", timeout, )?; @@ -83,7 +85,7 @@ pub(super) fn clone_git_source( let mut sparse_checkout = git_command(); sparse_checkout .arg("-C") - .arg(destination) + .arg(&git_destination) .arg("sparse-checkout") .arg("set") .args(sparse_paths); @@ -97,14 +99,14 @@ pub(super) fn clone_git_source( let output = run_git_command_with_timeout( git_command() .arg("-C") - .arg(destination) + .arg(&git_destination) .arg("checkout") .arg(ref_name.unwrap_or("HEAD")), "git checkout marketplace ref", timeout, )?; ensure_git_success(&output, "git checkout marketplace ref")?; - git_worktree_revision(destination, timeout) + git_worktree_revision(&git_destination, timeout) } fn git_worktree_revision(destination: &Path, timeout: Duration) -> Result { @@ -139,6 +141,28 @@ fn git_command() -> Command { command } +#[cfg(windows)] +fn git_path_arg(path: &Path) -> PathBuf { + strip_windows_verbatim_path_prefix(&path.to_string_lossy()) + .map(PathBuf::from) + .unwrap_or_else(|| path.to_path_buf()) +} + +#[cfg(not(windows))] +fn git_path_arg(path: &Path) -> PathBuf { + path.to_path_buf() +} + +#[cfg(any(windows, test))] +fn strip_windows_verbatim_path_prefix(path: &str) -> Option { + let stripped = path.strip_prefix(r"\\?\")?; + let stripped = stripped + .strip_prefix(r"UNC\") + .map(|unc_path| format!(r"\\{unc_path}")) + .unwrap_or_else(|| stripped.to_string()); + Some(stripped) +} + fn run_git_command_with_timeout( command: &mut Command, context: &str, @@ -201,6 +225,8 @@ fn ensure_git_success(output: &Output, context: &str) -> Result<(), String> { mod tests { use super::git_command; use super::is_full_git_sha; + use super::strip_windows_verbatim_path_prefix; + use pretty_assertions::assert_eq; use std::ffi::OsStr; #[test] @@ -226,6 +252,27 @@ mod tests { assert_eq!(command_env(&command, "PATH"), None); } + #[test] + fn strips_windows_verbatim_disk_prefix_for_git() { + assert_eq!( + strip_windows_verbatim_path_prefix(r"\\?\C:\Users\alice\marketplace"), + Some(r"C:\Users\alice\marketplace".to_string()) + ); + } + + #[test] + fn strips_windows_verbatim_unc_prefix_for_git() { + assert_eq!( + strip_windows_verbatim_path_prefix(r"\\?\UNC\server\share\marketplace"), + Some(r"\\server\share\marketplace".to_string()) + ); + } + + #[test] + fn leaves_non_verbatim_path_without_rewrite() { + assert_eq!(strip_windows_verbatim_path_prefix(r"C:\Users\alice"), None); + } + fn command_env<'a>( command: &'a std::process::Command, name: &str,