feat(cli): enable lazycodex codex installs

This commit is contained in:
YeonGyu-Kim
2026-05-30 21:32:57 +09:00
parent 675d828077
commit a789d6c2a5
14 changed files with 47 additions and 192 deletions
-7
View File
@@ -61,21 +61,14 @@ function stubOpenCodeSuccess(): void {
describe("runCliInstaller platform branching", () => {
const consoleLogMock = mock(() => {})
const consoleLog = console.log
const originalPublishLazycodex = process.env.OMO_PUBLISH_LAZYCODEX
beforeEach(() => {
consoleLogMock.mockClear()
console.log = consoleLogMock
process.env.OMO_PUBLISH_LAZYCODEX = "true"
})
afterEach(() => {
console.log = consoleLog
if (originalPublishLazycodex === undefined) {
delete process.env.OMO_PUBLISH_LAZYCODEX
} else {
process.env.OMO_PUBLISH_LAZYCODEX = originalPublishLazycodex
}
mock.restore()
})
-7
View File
@@ -9,7 +9,6 @@ describe("runCliInstaller", () => {
const mockConsoleError = mock(() => {})
const originalConsoleLog = console.log
const originalConsoleError = console.error
const originalPublishLazycodex = process.env.OMO_PUBLISH_LAZYCODEX
beforeEach(() => {
console.log = mockConsoleLog
@@ -21,11 +20,6 @@ describe("runCliInstaller", () => {
afterEach(() => {
console.log = originalConsoleLog
console.error = originalConsoleError
if (originalPublishLazycodex === undefined) {
delete process.env.OMO_PUBLISH_LAZYCODEX
} else {
process.env.OMO_PUBLISH_LAZYCODEX = originalPublishLazycodex
}
mock.restore()
})
@@ -134,7 +128,6 @@ describe("runCliInstaller", () => {
it("skips OpenCode checks and writes for platform=codex", async () => {
// given
process.env.OMO_PUBLISH_LAZYCODEX = "true"
const detectSpy = spyOn(configManager, "detectCurrentConfig")
const installedSpy = spyOn(configManager, "isOpenCodeInstalled")
const versionSpy = spyOn(configManager, "getOpenCodeVersion")
+1 -5
View File
@@ -6,7 +6,6 @@ import { doctor } from "./doctor"
import { refreshModelCapabilities } from "./refresh-model-capabilities"
import { createMcpOAuthCommand } from "./mcp-oauth"
import { boulder } from "./boulder"
import { isLazycodexPublishingEnabled } from "./lazycodex-feature-flag"
import type { InstallArgs } from "./types"
import type { RunOptions } from "./run"
import type { GetLocalVersionOptions } from "./get-local-version/types"
@@ -33,14 +32,11 @@ type InstallCommandOptions = {
readonly skipAuth?: boolean
}
type Environment = Readonly<Record<string, string | undefined>>
export function resolveInstallArgs(
options: InstallCommandOptions,
invocationName: string | undefined = process.env.OMO_INVOCATION_NAME,
env: Environment = process.env,
): InstallArgs {
const defaultPlatform = invocationName === "lazycodex" && isLazycodexPublishingEnabled(env) ? "codex" : undefined
const defaultPlatform = invocationName === "lazycodex" ? "codex" : undefined
return {
tui: options.tui !== false,
@@ -7,7 +7,6 @@ import { argsToConfig } from "../install-validators"
describe("lazycodex install routing", () => {
const originalInvocationName = process.env.OMO_INVOCATION_NAME
const originalPublishLazycodex = process.env.OMO_PUBLISH_LAZYCODEX
afterEach(() => {
if (originalInvocationName === undefined) {
@@ -15,38 +14,11 @@ describe("lazycodex install routing", () => {
} else {
process.env.OMO_INVOCATION_NAME = originalInvocationName
}
if (originalPublishLazycodex === undefined) {
delete process.env.OMO_PUBLISH_LAZYCODEX
} else {
process.env.OMO_PUBLISH_LAZYCODEX = originalPublishLazycodex
}
})
test("leaves lazycodex invocation unresolved when lazycodex publishing is disabled", () => {
test("defaults platform to codex when invoked as lazycodex", () => {
// given
process.env.OMO_INVOCATION_NAME = "lazycodex"
delete process.env.OMO_PUBLISH_LAZYCODEX
// when
const args = resolveInstallArgs({
tui: false,
claude: "no",
gemini: "no",
copilot: "no",
})
const config = argsToConfig(args)
// then
expect(args.platform).toBeUndefined()
expect(config.hasCodex).toBe(false)
expect(config.hasOpenCode).toBe(true)
})
test("defaults platform to codex when invoked as lazycodex with lazycodex publishing enabled", () => {
// given
process.env.OMO_INVOCATION_NAME = "lazycodex"
process.env.OMO_PUBLISH_LAZYCODEX = "true"
// when
const args = resolveInstallArgs({
@@ -63,10 +35,9 @@ describe("lazycodex install routing", () => {
expect(config.hasOpenCode).toBe(false)
})
test("respects explicit --platform=both when lazycodex publishing is enabled", () => {
test("respects explicit --platform=both", () => {
// given
process.env.OMO_INVOCATION_NAME = "lazycodex"
process.env.OMO_PUBLISH_LAZYCODEX = "true"
// when
const args = resolveInstallArgs({
+1 -12
View File
@@ -59,24 +59,13 @@ describe("install platform resolution", () => {
expect(args.platform).toBe("opencode")
})
test("leaves lazycodex install unresolved when lazycodex publishing is disabled", () => {
test("defaults lazycodex install to codex platform", () => {
// given
const invocationName = "lazycodex"
// when
const args = resolveInstallArgs({ tui: true }, invocationName)
// then
expect(args.platform).toBeUndefined()
})
test("defaults lazycodex install to codex platform when lazycodex publishing is enabled", () => {
// given
const invocationName = "lazycodex"
// when
const args = resolveInstallArgs({ tui: true }, invocationName, { OMO_PUBLISH_LAZYCODEX: "true" })
// then
expect(args.platform).toBe("codex")
})
+5 -21
View File
@@ -130,33 +130,19 @@ describe("validateNonTuiArgs", () => {
expect(result.errors).toContain("--copilot is required (values: no, yes)")
})
test("rejects codex-only non-TUI installs when lazycodex publishing is disabled", () => {
test("allows codex-only non-TUI installs", () => {
// #given
const args: InstallArgs = { tui: false, platform: "codex" }
// #when
const result = validateNonTuiArgs(args)
// #then
expect(result.valid).toBe(false)
expect(result.errors).toContain(
"Codex platform install is disabled. Set OMO_PUBLISH_LAZYCODEX=true to enable LazyCodex publish/install.",
)
})
test("allows codex-only non-TUI installs with lazycodex publishing enabled", () => {
// #given
const args: InstallArgs = { tui: false, platform: "codex" }
// #when
const result = validateNonTuiArgs(args, { OMO_PUBLISH_LAZYCODEX: "true" })
// #then
expect(result.valid).toBe(true)
expect(result.errors).toEqual([])
})
test("rejects platform=both when lazycodex publishing is disabled", () => {
test("allows platform=both without a LazyCodex publish flag", () => {
// #given
const args = createArgs({ platform: "both" })
@@ -164,10 +150,8 @@ describe("validateNonTuiArgs", () => {
const result = validateNonTuiArgs(args)
// #then
expect(result.valid).toBe(false)
expect(result.errors).toContain(
"Codex platform install is disabled. Set OMO_PUBLISH_LAZYCODEX=true to enable LazyCodex publish/install.",
)
expect(result.valid).toBe(true)
expect(result.errors).toEqual([])
})
test("rejects OpenCode flags for codex-only non-TUI installs", () => {
@@ -175,7 +159,7 @@ describe("validateNonTuiArgs", () => {
const args = createArgs({ platform: "codex", claude: "yes" })
// #when
const result = validateNonTuiArgs(args, { OMO_PUBLISH_LAZYCODEX: "true" })
const result = validateNonTuiArgs(args)
// #then
expect(result.valid).toBe(false)
+1 -14
View File
@@ -7,11 +7,6 @@ import type {
InstallConfig,
InstallPlatform,
} from "./types"
import {
LAZYCODEX_DISABLED_MESSAGE,
isLazycodexPublishingEnabled,
platformRequiresLazycodex,
} from "./lazycodex-feature-flag"
export const SYMBOLS = {
check: color.green("[OK]"),
@@ -24,7 +19,6 @@ export const SYMBOLS = {
}
const ANSI_COLOR_PATTERN = new RegExp("\u001b\\[[0-9;]*m", "g")
type Environment = Readonly<Record<string, string | undefined>>
function formatProvider(name: string, enabled: boolean, detail?: string): string {
const status = enabled ? SYMBOLS.check : color.dim("○")
@@ -125,19 +119,12 @@ export function printBox(content: string, title?: string): void {
console.log()
}
export function validateNonTuiArgs(
args: InstallArgs,
env: Environment = process.env,
): { valid: boolean; errors: string[] } {
export function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string[] } {
const errors: string[] = []
const platform = resolvePlatform(args)
const hasOpenCode = platform === "opencode" || platform === "both"
const hasCodexOnly = platform === "codex"
if (platformRequiresLazycodex(platform) && !isLazycodexPublishingEnabled(env)) {
errors.push(LAZYCODEX_DISABLED_MESSAGE)
}
if (hasOpenCode && args.claude === undefined) {
errors.push("--claude is required (values: no, yes, max20)")
} else if (args.claude !== undefined && !["no", "yes", "max20"].includes(args.claude)) {
-15
View File
@@ -1,15 +0,0 @@
import type { InstallPlatform } from "./types"
export const LAZYCODEX_PUBLISH_FLAG = "OMO_PUBLISH_LAZYCODEX"
export const LAZYCODEX_DISABLED_MESSAGE =
"Codex platform install is disabled. Set OMO_PUBLISH_LAZYCODEX=true to enable LazyCodex publish/install."
type Environment = Readonly<Record<string, string | undefined>>
export function isLazycodexPublishingEnabled(env: Environment = process.env): boolean {
return env[LAZYCODEX_PUBLISH_FLAG] === "true"
}
export function platformRequiresLazycodex(platform: InstallPlatform | undefined): boolean {
return platform === "codex" || platform === "both"
}
+12 -8
View File
@@ -46,12 +46,12 @@ describe("promptInstallPlatform", () => {
mock.restore()
})
test("offers OpenCode, Codex, and Both choices when lazycodex publishing is enabled", async () => {
test("offers OpenCode, Codex, and Both choices", async () => {
// given
const selectSpy = spyOn(p, "select").mockResolvedValue("opencode")
// when
const value = await prompts.promptInstallPlatform("opencode", true)
const value = await prompts.promptInstallPlatform("opencode")
// then
expect(value).toBe("opencode")
@@ -66,19 +66,23 @@ describe("promptInstallPlatform", () => {
})
})
test("hides Codex platform choices when lazycodex publishing is disabled", async () => {
test("preserves Codex as the initial platform", async () => {
// given
const selectSpy = spyOn(p, "select").mockResolvedValue("opencode")
const selectSpy = spyOn(p, "select").mockResolvedValue("codex")
// when
const value = await prompts.promptInstallPlatform("codex", false)
const value = await prompts.promptInstallPlatform("codex")
// then
expect(value).toBe("opencode")
expect(value).toBe("codex")
expect(selectSpy).toHaveBeenCalledTimes(1)
expect(selectSpy.mock.calls[0]?.[0]).toMatchObject({
initialValue: "opencode",
options: [{ value: "opencode" }],
initialValue: "codex",
options: [
{ value: "opencode" },
{ value: "codex" },
{ value: "both" },
],
})
})
})
+3 -11
View File
@@ -7,7 +7,6 @@ import type {
InstallPlatform,
} from "./types"
import { detectedToInitialValues } from "./install-validators"
import { isLazycodexPublishingEnabled } from "./lazycodex-feature-flag"
async function selectOrCancel<TValue extends Readonly<string | boolean | number>>(params: {
message: string
@@ -30,24 +29,17 @@ async function selectOrCancel<TValue extends Readonly<string | boolean | number>
export async function promptInstallPlatform(
initialValue: InstallPlatform = "opencode",
lazycodexEnabled = isLazycodexPublishingEnabled(),
): Promise<InstallPlatform | null> {
const options: Option<InstallPlatform>[] = [
{ value: "opencode", label: "OpenCode", hint: "Install OpenCode plugin only" },
{ value: "codex", label: "Codex", hint: "Install Codex harness adapter only" },
{ value: "both", label: "Both", hint: "Install OpenCode plugin and Codex adapter" },
]
if (lazycodexEnabled) {
options.push(
{ value: "codex", label: "Codex", hint: "Install Codex harness adapter only" },
{ value: "both", label: "Both", hint: "Install OpenCode plugin and Codex adapter" },
)
}
const safeInitialValue = lazycodexEnabled || initialValue === "opencode" ? initialValue : "opencode"
return selectOrCancel<InstallPlatform>({
message: "Which platform do you want to install?",
options,
initialValue: safeInitialValue,
initialValue,
})
}
+1 -32
View File
@@ -19,7 +19,6 @@ function createMockSpinner(): ReturnType<typeof p.spinner> {
describe("runTuiInstaller", () => {
const originalIsStdinTty = process.stdin.isTTY
const originalIsStdoutTty = process.stdout.isTTY
const originalPublishLazycodex = process.env.OMO_PUBLISH_LAZYCODEX
beforeEach(() => {
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true })
@@ -29,11 +28,6 @@ describe("runTuiInstaller", () => {
afterEach(() => {
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: originalIsStdinTty })
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: originalIsStdoutTty })
if (originalPublishLazycodex === undefined) {
delete process.env.OMO_PUBLISH_LAZYCODEX
} else {
process.env.OMO_PUBLISH_LAZYCODEX = originalPublishLazycodex
}
})
it("blocks installation when OpenCode is below the minimum version", async () => {
@@ -57,7 +51,6 @@ describe("runTuiInstaller", () => {
hasKimiForCoding: false,
hasOpencodeGo: false,
hasVercelAiGateway: false,
codexAutonomous: false,
}),
spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true),
spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.3.9"),
@@ -83,29 +76,6 @@ describe("runTuiInstaller", () => {
outroSpy.mockRestore()
})
it("blocks codex platform when lazycodex publishing is disabled", async () => {
// given
delete process.env.OMO_PUBLISH_LAZYCODEX
const platformSpy = spyOn(tuiInstallPrompts, "promptInstallPlatform").mockResolvedValue("codex")
const promptConfigSpy = spyOn(tuiInstallPrompts, "promptInstallConfig")
const logErrorSpy = spyOn(p.log, "error").mockImplementation(() => undefined)
const outroSpy = spyOn(p, "outro").mockImplementation(() => undefined)
// when
const result = await runTuiInstaller({ tui: true, platform: "codex" }, "3.16.0")
// then
expect(result).toBe(1)
expect(platformSpy).toHaveBeenCalled()
expect(promptConfigSpy).not.toHaveBeenCalled()
expect(logErrorSpy).toHaveBeenCalled()
platformSpy.mockRestore()
promptConfigSpy.mockRestore()
logErrorSpy.mockRestore()
outroSpy.mockRestore()
})
it("proceeds when OpenCode meets the minimum version", async () => {
// given
const restoreSpies = [
@@ -132,7 +102,6 @@ describe("runTuiInstaller", () => {
hasKimiForCoding: false,
hasOpencodeGo: false,
hasVercelAiGateway: false,
codexAutonomous: false,
}),
spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true),
spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0"),
@@ -175,7 +144,6 @@ describe("runTuiInstaller", () => {
it("skips OpenCode checks and writes when platform is codex", async () => {
// given
process.env.OMO_PUBLISH_LAZYCODEX = "true"
const restoreSpies = [
spyOn(p, "spinner").mockReturnValue(createMockSpinner()),
spyOn(p, "intro").mockImplementation(() => undefined),
@@ -200,6 +168,7 @@ describe("runTuiInstaller", () => {
hasKimiForCoding: false,
hasOpencodeGo: false,
hasVercelAiGateway: false,
codexAutonomous: false,
}),
]
const detectConfigSpy = spyOn(configManager, "detectCurrentConfig")
-10
View File
@@ -13,11 +13,6 @@ import { detectedToInitialValues, formatConfigSummary, SYMBOLS } from "./install
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
import { promptInstallConfig, promptInstallPlatform } from "./tui-install-prompts"
import { runCodexInstaller } from "./install-codex"
import {
LAZYCODEX_DISABLED_MESSAGE,
isLazycodexPublishingEnabled,
platformRequiresLazycodex,
} from "./lazycodex-feature-flag"
import { STAR_REPOSITORIES, formatGitHubStarCommand } from "./star-request"
export async function runTuiInstaller(args: InstallArgs, version: string): Promise<number> {
@@ -28,11 +23,6 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
const selectedPlatform = await promptInstallPlatform(args.platform ?? "opencode")
if (!selectedPlatform) return 1
if (platformRequiresLazycodex(selectedPlatform) && !isLazycodexPublishingEnabled()) {
p.log.error(LAZYCODEX_DISABLED_MESSAGE)
p.outro(color.red("Installation blocked."))
return 1
}
const hasOpenCode = selectedPlatform === "opencode" || selectedPlatform === "both"
const detected = hasOpenCode