Add Codex CLI as a Windows FMA (#42397)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added comprehensive support for managing Codex CLI (OpenAI's coding agent) on Windows systems, including automated installation, uninstallation, and verification that installed binaries match expected versions * Integrated Codex CLI icon component into the software interface for improved visual identification and enhanced user experience when managing this application <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/42397) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
copilot-swe-agent[bot]
Cursor
parent
19de907416
commit
9afdb43567
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -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,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Reference in New Issue
Block a user