Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 21 additions & 30 deletions src/resources/codex/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import {
CreatePlan,
DestroyPlan,
ExampleConfig,
PackageManager,
Resource,
ResourceSettings,
SpawnError,
SpawnStatus,
Utils,
getPty,
z,
} from '@codifycli/plugin-core';
Expand Down Expand Up @@ -151,47 +152,37 @@ export class CodexResource extends Resource<CodexConfig> {
}

async refresh(): Promise<Partial<CodexConfig> | null> {
const codexBin = path.join(os.homedir(), '.local', 'bin', 'codex');
try {
await fs.access(codexBin);
} catch {
return null;
}

return {};
const $ = getPty();
const { status } = await $.spawnSafe('which codex');
return status === SpawnStatus.SUCCESS ? {} : null;
}

async create(_plan: CreatePlan<CodexConfig>): Promise<void> {
const $ = getPty();

const installCmd = 'bash -c "curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 sh"';

// The official installer resolves "latest" via GitHub's unauthenticated release API,
// which is IP rate-limited and intermittently returns 403 on shared CI runners.
// Retry with backoff rather than failing outright on what is usually a transient block.
const maxAttempts = 4;
let result;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
result = await $.spawnSafe(installCmd, { interactive: true });
if (result.status === SpawnStatus.SUCCESS) {
break;
}

if (attempt < maxAttempts) {
await new Promise((resolve) => setTimeout(resolve, attempt * 15_000));
}
if (Utils.isMacOS()) {
// The Homebrew cask downloads a pinned release asset directly, avoiding the
// unauthenticated GitHub release API that the official curl installer relies on
// to resolve "latest" (that API is IP rate-limited and returns 403 on shared CI runners).
await Utils.installViaPkgMgr('codex', { [PackageManager.BREW]: { cask: true } }, PackageManager.BREW);
return;
}

if (result!.status !== SpawnStatus.SUCCESS) {
throw new SpawnError(installCmd, result!.exitCode, result!.data);
}
const $ = getPty();
await $.spawn(
'bash -c "curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 sh"',
{ interactive: true },
);

// Ensure PATH is updated so subsequent lifecycle methods can call `codex`
const localBin = path.join(os.homedir(), '.local', 'bin');
process.env['PATH'] = `${localBin}:${process.env['PATH'] ?? ''}`;
}

async destroy(_plan: DestroyPlan<CodexConfig>): Promise<void> {
if (Utils.isMacOS()) {
await Utils.uninstallViaPkgMgr('codex', { [PackageManager.BREW]: { cask: true } }, PackageManager.BREW);
return;
}

// Native uninstall: remove the binary and standalone release artifacts
await fs.rm(path.join(os.homedir(), '.local', 'bin', 'codex'), { force: true });
await fs.rm(path.join(os.homedir(), '.codex', 'packages', 'standalone'), { recursive: true, force: true });
Expand Down