diff --git a/cmd/maintained-apps/validate/windows.go b/cmd/maintained-apps/validate/windows.go index 50ca675cf8..dafb4feb3e 100644 --- a/cmd/maintained-apps/validate/windows.go +++ b/cmd/maintained-apps/validate/windows.go @@ -174,6 +174,74 @@ func appExists(ctx context.Context, logger *slog.Logger, appName, uniqueIdentifi } } + // OpenAI Codex CLI is a portable zip: it does not register in programs. Detect the binary via osquery file + PE version. + if uniqueIdentifier == "Codex CLI" { + ok, err := codexCLIExistsFromFile(execTimeout, logger, appVersion, appPath) + if err != nil { + return false, err + } + if ok { + return true, nil + } + } + + return false, nil +} + +func codexCLIExistsFromFile(ctx context.Context, logger *slog.Logger, appVersion, appPath string) (bool, error) { + candidates := make([]string, 0, 3) + if appPath != "" { + candidates = append(candidates, filepath.Join(appPath, "codex.exe")) + } + if pf := os.Getenv("ProgramFiles"); pf != "" { + candidates = append(candidates, filepath.Join(pf, "Codex CLI", "codex.exe")) + } + if la := os.Getenv("LOCALAPPDATA"); la != "" { + candidates = append(candidates, filepath.Join(la, "Programs", "Codex CLI", "codex.exe")) + } + + seen := make(map[string]struct{}) + for _, exePath := range candidates { + if _, dup := seen[exePath]; dup { + continue + } + seen[exePath] = struct{}{} + + if err := validateSqlInput(exePath); err != nil { + continue + } + + escaped := strings.ReplaceAll(exePath, "'", "''") + query := `SELECT file_version FROM file WHERE path = '` + escaped + `'` + cmd := exec.CommandContext(ctx, "osqueryi", "--json", query) + output, err := cmd.CombinedOutput() + if err != nil { + logger.ErrorContext(ctx, fmt.Sprintf("osquery output: %s", string(output))) + return false, fmt.Errorf("executing osquery file lookup: %w", err) + } + + type fileResult struct { + FileVersion string `json:"file_version"` + } + var results []fileResult + if err := json.Unmarshal(output, &results); err != nil { + logger.ErrorContext(ctx, fmt.Sprintf("osquery output: %s", string(output))) + return false, fmt.Errorf("parsing osquery JSON output: %w", err) + } + if len(results) == 0 || results[0].FileVersion == "" { + continue + } + + fileVer := results[0].FileVersion + logger.InfoContext(ctx, fmt.Sprintf("Found Codex CLI binary at %s, file version: %s", exePath, fileVer)) + + if fileVer == appVersion || + strings.HasPrefix(fileVer, appVersion+".") || + strings.HasPrefix(appVersion, fileVer+".") { + return true, nil + } + } + return false, nil } diff --git a/ee/maintained-apps/ingesters/winget/ingester.go b/ee/maintained-apps/ingesters/winget/ingester.go index 99cbde9ca0..204c55e1ab 100644 --- a/ee/maintained-apps/ingesters/winget/ingester.go +++ b/ee/maintained-apps/ingesters/winget/ingester.go @@ -381,6 +381,9 @@ func (i *wingetIngester) ingestOne(ctx context.Context, input inputApp) (*mainta } out.Queries = setUpExistsQuery(input.FuzzyMatchName, name, publisher) + if input.ExistsQuery != "" { + out.Queries.Exists = input.ExistsQuery + } out.InstallScript = installScript processedUninstallScript, err := preProcessUninstallScript(uninstallScript, productCode) if err != nil { @@ -515,6 +518,8 @@ type inputApp struct { ProgramPublisher string `json:"program_publisher"` UninstallType string `json:"uninstall_type"` FuzzyMatchName fuzzyMatch `json:"fuzzy_match_name"` + // ExistsQuery overrides the default programs-table exists query (e.g. portable zip installs). + ExistsQuery string `json:"exists_query,omitempty"` // Whether to use "no_check" instead of the app's hash (e.g. for non-pinned download URLs) IgnoreHash bool `json:"ignore_hash"` DefaultCategories []string `json:"default_categories"` diff --git a/ee/maintained-apps/inputs/winget/codex-cli.json b/ee/maintained-apps/inputs/winget/codex-cli.json new file mode 100644 index 0000000000..bd800cb6b3 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/codex-cli.json @@ -0,0 +1,13 @@ +{ + "name": "Codex CLI", + "slug": "codex-cli/windows", + "package_identifier": "OpenAI.Codex", + "unique_identifier": "Codex CLI", + "install_script_path": "ee/maintained-apps/inputs/winget/scripts/codex-cli_install.ps1", + "uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/codex-cli_uninstall.ps1", + "exists_query": "SELECT 1 FROM file WHERE path = 'C:\\Program Files\\Codex CLI\\codex.exe' OR path LIKE '%\\AppData\\Local\\Programs\\Codex CLI\\codex.exe';", + "installer_arch": "x64", + "installer_type": "zip", + "installer_scope": "user", + "default_categories": ["Developer tools"] +} diff --git a/ee/maintained-apps/inputs/winget/scripts/codex-cli_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/codex-cli_install.ps1 new file mode 100644 index 0000000000..000aca9fca --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/codex-cli_install.ps1 @@ -0,0 +1,59 @@ +# Codex ships as a .zip of portable binaries (winget NestedInstallerType: portable). +# INSTALLER_PATH points at the downloaded .zip. +# Prefer Program Files (matches Fleet validation + managed installs); fall back to %LOCALAPPDATA% without admin. + +$ErrorActionPreference = "Stop" +$zipPath = "${env:INSTALLER_PATH}" +$machineRoot = Join-Path $env:ProgramFiles "Codex CLI" +$userRoot = Join-Path $env:LOCALAPPDATA "Programs\Codex CLI" +$extractDir = Join-Path $env:TEMP ("codex-winget-extract-" + [Guid]::NewGuid().ToString()) + +if (-not (Test-Path -LiteralPath $machineRoot)) { + try { + New-Item -ItemType Directory -Path $machineRoot -Force -ErrorAction Stop | Out-Null + $installRoot = $machineRoot + } catch { + New-Item -ItemType Directory -Path $userRoot -Force | Out-Null + $installRoot = $userRoot + } +} else { + $installRoot = $machineRoot +} + +try { + if (-not (Test-Path -LiteralPath $zipPath)) { + Write-Host "Installer not found: $zipPath" + Exit 1 + } + + New-Item -ItemType Directory -Path $installRoot -Force | Out-Null + New-Item -ItemType Directory -Path $extractDir -Force | Out-Null + Expand-Archive -LiteralPath $zipPath -DestinationPath $extractDir -Force + + # Matches winget manifest NestedInstallerFiles for x64 + $mainExe = Join-Path $extractDir "codex-x86_64-pc-windows-msvc.exe" + if (-not (Test-Path -LiteralPath $mainExe)) { + Write-Host "Expected binary codex-x86_64-pc-windows-msvc.exe not found in archive" + Exit 1 + } + + $destExe = Join-Path $installRoot "codex.exe" + Copy-Item -LiteralPath $mainExe -Destination $destExe -Force + + foreach ($extra in @( + "codex-command-runner.exe", + "codex-windows-sandbox-setup.exe" + )) { + $src = Join-Path $extractDir $extra + if (Test-Path -LiteralPath $src) { + Copy-Item -LiteralPath $src -Destination (Join-Path $installRoot $extra) -Force + } + } + + Exit 0 +} catch { + Write-Host "Error: $_" + Exit 1 +} finally { + Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/ee/maintained-apps/inputs/winget/scripts/codex-cli_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/codex-cli_uninstall.ps1 new file mode 100644 index 0000000000..0d3734c0e1 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/codex-cli_uninstall.ps1 @@ -0,0 +1,13 @@ +# Removes layouts created by codex-cli_install.ps1 (machine and per-user fallbacks). + +$ErrorActionPreference = "Continue" +foreach ($installRoot in @( + (Join-Path $env:ProgramFiles "Codex CLI"), + (Join-Path $env:LOCALAPPDATA "Programs\Codex CLI") +)) { + if (Test-Path -LiteralPath $installRoot) { + Remove-Item -LiteralPath $installRoot -Recurse -Force + } +} + +Exit 0 diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index a59d64cb7b..e7b0fdb3c0 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -463,6 +463,13 @@ "unique_identifier": "Cloudflare WARP", "description": "Cloudflare WARP enhances internet safety and performance by encrypting your data and optimizing connections for privacy." }, + { + "name": "Codex CLI", + "slug": "codex-cli/windows", + "platform": "windows", + "unique_identifier": "Codex CLI", + "description": "Codex CLI is OpenAI’s coding agent that you can run locally from your terminal." + }, { "name": "Connect Fonts", "slug": "connect-fonts/darwin", diff --git a/ee/maintained-apps/outputs/codex-cli/windows.json b/ee/maintained-apps/outputs/codex-cli/windows.json new file mode 100644 index 0000000000..3d3518f260 --- /dev/null +++ b/ee/maintained-apps/outputs/codex-cli/windows.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "0.116.0", + "queries": { + "exists": "SELECT 1 FROM file WHERE path = 'C:\\Program Files\\Codex CLI\\codex.exe' OR path LIKE '%\\AppData\\Local\\Programs\\Codex CLI\\codex.exe';", + "patch": "" + }, + "installer_url": "https://github.com/openai/codex/releases/download/rust-v0.116.0/codex-x86_64-pc-windows-msvc.exe.zip", + "install_script_ref": "8bb4b68b", + "uninstall_script_ref": "7e1fe544", + "sha256": "f3a64ca3d389224404c1eb654015a868247e4f59bd300e3cdc05f8933f667f8a", + "default_categories": [ + "Developer tools" + ] + } + ], + "refs": { + "7e1fe544": "# Removes layouts created by codex-cli_install.ps1 (machine and per-user fallbacks).\n\n$ErrorActionPreference = \"Continue\"\nforeach ($installRoot in @(\n (Join-Path $env:ProgramFiles \"Codex CLI\"),\n (Join-Path $env:LOCALAPPDATA \"Programs\\Codex CLI\")\n)) {\n if (Test-Path -LiteralPath $installRoot) {\n Remove-Item -LiteralPath $installRoot -Recurse -Force\n }\n}\n\nExit 0\n", + "8bb4b68b": "# Codex ships as a .zip of portable binaries (winget NestedInstallerType: portable).\n# INSTALLER_PATH points at the downloaded .zip.\n# Prefer Program Files (matches Fleet validation + managed installs); fall back to %LOCALAPPDATA% without admin.\n\n$ErrorActionPreference = \"Stop\"\n$zipPath = \"${env:INSTALLER_PATH}\"\n$machineRoot = Join-Path $env:ProgramFiles \"Codex CLI\"\n$userRoot = Join-Path $env:LOCALAPPDATA \"Programs\\Codex CLI\"\n$extractDir = Join-Path $env:TEMP (\"codex-winget-extract-\" + [Guid]::NewGuid().ToString())\n\nif (-not (Test-Path -LiteralPath $machineRoot)) {\n try {\n New-Item -ItemType Directory -Path $machineRoot -Force -ErrorAction Stop | Out-Null\n $installRoot = $machineRoot\n } catch {\n New-Item -ItemType Directory -Path $userRoot -Force | Out-Null\n $installRoot = $userRoot\n }\n} else {\n $installRoot = $machineRoot\n}\n\ntry {\n if (-not (Test-Path -LiteralPath $zipPath)) {\n Write-Host \"Installer not found: $zipPath\"\n Exit 1\n }\n\n New-Item -ItemType Directory -Path $installRoot -Force | Out-Null\n New-Item -ItemType Directory -Path $extractDir -Force | Out-Null\n Expand-Archive -LiteralPath $zipPath -DestinationPath $extractDir -Force\n\n # Matches winget manifest NestedInstallerFiles for x64\n $mainExe = Join-Path $extractDir \"codex-x86_64-pc-windows-msvc.exe\"\n if (-not (Test-Path -LiteralPath $mainExe)) {\n Write-Host \"Expected binary codex-x86_64-pc-windows-msvc.exe not found in archive\"\n Exit 1\n }\n\n $destExe = Join-Path $installRoot \"codex.exe\"\n Copy-Item -LiteralPath $mainExe -Destination $destExe -Force\n\n foreach ($extra in @(\n \"codex-command-runner.exe\",\n \"codex-windows-sandbox-setup.exe\"\n )) {\n $src = Join-Path $extractDir $extra\n if (Test-Path -LiteralPath $src) {\n Copy-Item -LiteralPath $src -Destination (Join-Path $installRoot $extra) -Force\n }\n }\n\n Exit 0\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n} finally {\n Remove-Item -LiteralPath $extractDir -Recurse -Force -ErrorAction SilentlyContinue\n}\n" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/CodexCli.tsx b/frontend/pages/SoftwarePage/components/icons/CodexCli.tsx new file mode 100644 index 0000000000..b869ce4569 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/CodexCli.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import type { SVGProps } from "react"; + +const CodexCli = (props: SVGProps) => ( + + + +); +export default CodexCli; diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index 5f723a0e1d..7af7af906e 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -9,6 +9,7 @@ import Backblaze from "./Backblaze"; import BetterDisplay from "./BetterDisplay"; import Cavalry from "./Cavalry"; import Charles from "./Charles"; +import CodexCli from "./CodexCli"; import ConnectFonts from "./ConnectFonts"; import CrashPlan from "./CrashPlan"; import DruvaInSync from "./DruvaInSync"; @@ -320,6 +321,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { "clockify desktop": ClockifyDesktop, cloudflare: Cloudflare, code: VisualStudioCode, + "codex cli": CodexCli, "company portal": IntuneCompanyPortal, "connect fonts": ConnectFonts, crashplan: CrashPlan, diff --git a/website/assets/images/app-icon-codex-cli-60x60@2x.png b/website/assets/images/app-icon-codex-cli-60x60@2x.png new file mode 100644 index 0000000000..03ce31400e Binary files /dev/null and b/website/assets/images/app-icon-codex-cli-60x60@2x.png differ