ci(lazycodex): gate publish and install

This commit is contained in:
YeonGyu-Kim
2026-05-29 12:18:42 +09:00
parent 4bf9095456
commit 0edf087117
14 changed files with 227 additions and 23 deletions
+17 -5
View File
@@ -22,6 +22,11 @@ on:
required: false required: false
type: boolean type: boolean
default: false default: false
publish_lazycodex:
description: "Publish lazycodex npm alias and sync Codex marketplace"
required: false
type: boolean
default: false
concurrency: ${{ github.workflow }}-${{ github.ref }} concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -72,10 +77,11 @@ jobs:
id-token: write id-token: write
contents: read contents: read
steps: steps:
- name: Verify trusted publisher for all 25 packages - name: Verify trusted publisher for release packages
env: env:
REPO: code-yeongyu/oh-my-openagent REPO: code-yeongyu/oh-my-openagent
WORKFLOW_FILE: publish.yml WORKFLOW_FILE: publish.yml
PUBLISH_LAZYCODEX: ${{ inputs.publish_lazycodex }}
run: | run: |
OIDC_TOKEN=$(curl -sH "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ OIDC_TOKEN=$(curl -sH "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=npm:registry.npmjs.org" \ "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=npm:registry.npmjs.org" \
@@ -87,7 +93,10 @@ jobs:
fi fi
PLATFORMS=(darwin-arm64 darwin-x64 darwin-x64-baseline linux-x64 linux-x64-baseline linux-arm64 linux-x64-musl linux-x64-musl-baseline linux-arm64-musl windows-x64 windows-x64-baseline) PLATFORMS=(darwin-arm64 darwin-x64 darwin-x64-baseline linux-x64 linux-x64-baseline linux-arm64 linux-x64-musl linux-x64-musl-baseline linux-arm64-musl windows-x64 windows-x64-baseline)
ALL_PACKAGES=(oh-my-opencode oh-my-openagent lazycodex) ALL_PACKAGES=(oh-my-opencode oh-my-openagent)
if [ "${PUBLISH_LAZYCODEX}" = "true" ]; then
ALL_PACKAGES+=(lazycodex)
fi
for plat in "${PLATFORMS[@]}"; do for plat in "${PLATFORMS[@]}"; do
ALL_PACKAGES+=("oh-my-opencode-${plat}") ALL_PACKAGES+=("oh-my-opencode-${plat}")
ALL_PACKAGES+=("oh-my-openagent-${plat}") ALL_PACKAGES+=("oh-my-openagent-${plat}")
@@ -327,6 +336,7 @@ jobs:
git checkout -- package.json git checkout -- package.json
- name: Check if lazycodex already published - name: Check if lazycodex already published
if: inputs.publish_lazycodex == true
id: check-lazycodex id: check-lazycodex
env: env:
VERSION: ${{ steps.version.outputs.version }} VERSION: ${{ steps.version.outputs.version }}
@@ -340,7 +350,7 @@ jobs:
fi fi
- name: Publish lazycodex - name: Publish lazycodex
if: steps.check-lazycodex.outputs.skip != 'true' if: inputs.publish_lazycodex == true && steps.check-lazycodex.outputs.skip != 'true'
continue-on-error: true continue-on-error: true
env: env:
VERSION: ${{ steps.version.outputs.version }} VERSION: ${{ steps.version.outputs.version }}
@@ -368,7 +378,7 @@ jobs:
fi fi
- name: Restore package.json after lazycodex publish attempt - name: Restore package.json after lazycodex publish attempt
if: always() && steps.check-lazycodex.outputs.skip != 'true' if: always() && inputs.publish_lazycodex == true && steps.check-lazycodex.outputs.skip != 'true'
run: | run: |
git checkout -- package.json git checkout -- package.json
@@ -452,12 +462,13 @@ jobs:
git push origin "v${VERSION}" git push origin "v${VERSION}"
- name: Require LazyCodex sync token - name: Require LazyCodex sync token
if: ${{ secrets.LAZYCODEX_SYNC_TOKEN == '' }} if: inputs.publish_lazycodex == true && secrets.LAZYCODEX_SYNC_TOKEN == ''
run: | run: |
echo "::error::LAZYCODEX_SYNC_TOKEN is required to push the Codex marketplace bundle to code-yeongyu/lazycodex." echo "::error::LAZYCODEX_SYNC_TOKEN is required to push the Codex marketplace bundle to code-yeongyu/lazycodex."
exit 1 exit 1
- name: Checkout LazyCodex marketplace - name: Checkout LazyCodex marketplace
if: inputs.publish_lazycodex == true
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: code-yeongyu/lazycodex repository: code-yeongyu/lazycodex
@@ -466,6 +477,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Sync LazyCodex Codex marketplace - name: Sync LazyCodex Codex marketplace
if: inputs.publish_lazycodex == true
env: env:
VERSION: ${{ needs.publish-main.outputs.version }} VERSION: ${{ needs.publish-main.outputs.version }}
run: | run: |
+11 -1
View File
@@ -59,21 +59,31 @@ describe("test workflows", () => {
expect(buildNeedsCodexMatrix, "Build must wait for Codex compatibility checks").toBe(true) expect(buildNeedsCodexMatrix, "Build must wait for Codex compatibility checks").toBe(true)
}) })
test("syncs the LazyCodex Codex marketplace bundle during release", () => { test("keeps LazyCodex deployment behind an explicit publish flag", () => {
// #given // #given
const workflow = readFileSync(new URL("../.github/workflows/publish.yml", import.meta.url), "utf8") const workflow = readFileSync(new URL("../.github/workflows/publish.yml", import.meta.url), "utf8")
// #when // #when
const appliesCodexPluginVersion = workflow.includes("packages/omo-codex/plugin/.codex-plugin/plugin.json") const appliesCodexPluginVersion = workflow.includes("packages/omo-codex/plugin/.codex-plugin/plugin.json")
const flagDefaultsOff = workflow.includes("publish_lazycodex:") &&
workflow.includes('description: "Publish lazycodex npm alias and sync Codex marketplace"') &&
workflow.includes("default: false")
const syncsLazycodexMarketplace = workflow.includes("bun run script/sync-lazycodex-marketplace.ts") const syncsLazycodexMarketplace = workflow.includes("bun run script/sync-lazycodex-marketplace.ts")
const pushesLazycodexMarketplace = workflow.includes("code-yeongyu/lazycodex") const pushesLazycodexMarketplace = workflow.includes("code-yeongyu/lazycodex")
const gatesLazycodexNpmPublish = workflow.includes("name: Publish lazycodex") &&
workflow.includes("if: inputs.publish_lazycodex == true && steps.check-lazycodex.outputs.skip != 'true'")
const gatesLazycodexMarketplaceSync = workflow.includes("name: Sync LazyCodex Codex marketplace") &&
workflow.includes("if: inputs.publish_lazycodex == true")
const requiresLazycodexSyncToken = workflow.includes("secrets.LAZYCODEX_SYNC_TOKEN == ''") && const requiresLazycodexSyncToken = workflow.includes("secrets.LAZYCODEX_SYNC_TOKEN == ''") &&
workflow.includes("token: ${{ secrets.LAZYCODEX_SYNC_TOKEN }}") workflow.includes("token: ${{ secrets.LAZYCODEX_SYNC_TOKEN }}")
// #then // #then
expect(appliesCodexPluginVersion, "release must version the Codex plugin manifest before marketplace sync").toBe(true) expect(appliesCodexPluginVersion, "release must version the Codex plugin manifest before marketplace sync").toBe(true)
expect(flagDefaultsOff, "LazyCodex deployment must default to disabled").toBe(true)
expect(syncsLazycodexMarketplace, "release must sync the LazyCodex marketplace bundle").toBe(true) expect(syncsLazycodexMarketplace, "release must sync the LazyCodex marketplace bundle").toBe(true)
expect(pushesLazycodexMarketplace, "release must target the LazyCodex repository").toBe(true) expect(pushesLazycodexMarketplace, "release must target the LazyCodex repository").toBe(true)
expect(gatesLazycodexNpmPublish, "lazycodex npm publish must require publish_lazycodex=true").toBe(true)
expect(gatesLazycodexMarketplaceSync, "LazyCodex marketplace push must require publish_lazycodex=true").toBe(true)
expect(requiresLazycodexSyncToken, "release must require a cross-repo token for LazyCodex push").toBe(true) expect(requiresLazycodexSyncToken, "release must require a cross-repo token for LazyCodex push").toBe(true)
}) })
}) })
+7
View File
@@ -60,13 +60,20 @@ function stubOpenCodeSuccess(): void {
describe("runCliInstaller platform branching", () => { describe("runCliInstaller platform branching", () => {
const consoleLog = console.log const consoleLog = console.log
const originalPublishLazycodex = process.env.OMO_PUBLISH_LAZYCODEX
beforeEach(() => { beforeEach(() => {
console.log = mock(() => {}) console.log = mock(() => {})
process.env.OMO_PUBLISH_LAZYCODEX = "true"
}) })
afterEach(() => { afterEach(() => {
console.log = consoleLog console.log = consoleLog
if (originalPublishLazycodex === undefined) {
delete process.env.OMO_PUBLISH_LAZYCODEX
} else {
process.env.OMO_PUBLISH_LAZYCODEX = originalPublishLazycodex
}
mock.restore() mock.restore()
}) })
+7
View File
@@ -9,6 +9,7 @@ describe("runCliInstaller", () => {
const mockConsoleError = mock(() => {}) const mockConsoleError = mock(() => {})
const originalConsoleLog = console.log const originalConsoleLog = console.log
const originalConsoleError = console.error const originalConsoleError = console.error
const originalPublishLazycodex = process.env.OMO_PUBLISH_LAZYCODEX
beforeEach(() => { beforeEach(() => {
console.log = mockConsoleLog console.log = mockConsoleLog
@@ -20,6 +21,11 @@ describe("runCliInstaller", () => {
afterEach(() => { afterEach(() => {
console.log = originalConsoleLog console.log = originalConsoleLog
console.error = originalConsoleError console.error = originalConsoleError
if (originalPublishLazycodex === undefined) {
delete process.env.OMO_PUBLISH_LAZYCODEX
} else {
process.env.OMO_PUBLISH_LAZYCODEX = originalPublishLazycodex
}
mock.restore() mock.restore()
}) })
@@ -128,6 +134,7 @@ describe("runCliInstaller", () => {
it("skips OpenCode checks and writes for platform=codex", async () => { it("skips OpenCode checks and writes for platform=codex", async () => {
// given // given
process.env.OMO_PUBLISH_LAZYCODEX = "true"
const detectSpy = spyOn(configManager, "detectCurrentConfig") const detectSpy = spyOn(configManager, "detectCurrentConfig")
const installedSpy = spyOn(configManager, "isOpenCodeInstalled") const installedSpy = spyOn(configManager, "isOpenCodeInstalled")
const versionSpy = spyOn(configManager, "getOpenCodeVersion") const versionSpy = spyOn(configManager, "getOpenCodeVersion")
+7 -1
View File
@@ -6,6 +6,7 @@ import { doctor } from "./doctor"
import { refreshModelCapabilities } from "./refresh-model-capabilities" import { refreshModelCapabilities } from "./refresh-model-capabilities"
import { createMcpOAuthCommand } from "./mcp-oauth" import { createMcpOAuthCommand } from "./mcp-oauth"
import { boulder } from "./boulder" import { boulder } from "./boulder"
import { isLazycodexPublishingEnabled } from "./lazycodex-feature-flag"
import type { InstallArgs } from "./types" import type { InstallArgs } from "./types"
import type { RunOptions } from "./run" import type { RunOptions } from "./run"
import type { GetLocalVersionOptions } from "./get-local-version/types" import type { GetLocalVersionOptions } from "./get-local-version/types"
@@ -31,17 +32,22 @@ type InstallCommandOptions = {
readonly skipAuth?: boolean readonly skipAuth?: boolean
} }
type Environment = Readonly<Record<string, string | undefined>>
export function resolveInstallArgs( export function resolveInstallArgs(
options: InstallCommandOptions, options: InstallCommandOptions,
invocationName: string | undefined = process.env.OMO_INVOCATION_NAME, invocationName: string | undefined = process.env.OMO_INVOCATION_NAME,
env: Environment = process.env,
): InstallArgs { ): InstallArgs {
const defaultPlatform = invocationName === "lazycodex" && isLazycodexPublishingEnabled(env) ? "codex" : undefined
return { return {
tui: options.tui !== false, tui: options.tui !== false,
claude: options.claude, claude: options.claude,
openai: options.openai, openai: options.openai,
gemini: options.gemini, gemini: options.gemini,
copilot: options.copilot, copilot: options.copilot,
platform: options.platform ?? (invocationName === "lazycodex" ? "codex" : undefined), platform: options.platform ?? defaultPlatform,
opencodeZen: options.opencodeZen, opencodeZen: options.opencodeZen,
zaiCodingPlan: options.zaiCodingPlan, zaiCodingPlan: options.zaiCodingPlan,
kimiForCoding: options.kimiForCoding, kimiForCoding: options.kimiForCoding,
@@ -7,18 +7,46 @@ import { argsToConfig } from "../install-validators"
describe("lazycodex install routing", () => { describe("lazycodex install routing", () => {
const originalInvocationName = process.env.OMO_INVOCATION_NAME const originalInvocationName = process.env.OMO_INVOCATION_NAME
const originalPublishLazycodex = process.env.OMO_PUBLISH_LAZYCODEX
afterEach(() => { afterEach(() => {
if (originalInvocationName === undefined) { if (originalInvocationName === undefined) {
delete process.env.OMO_INVOCATION_NAME delete process.env.OMO_INVOCATION_NAME
return } else {
process.env.OMO_INVOCATION_NAME = originalInvocationName
}
if (originalPublishLazycodex === undefined) {
delete process.env.OMO_PUBLISH_LAZYCODEX
} else {
process.env.OMO_PUBLISH_LAZYCODEX = originalPublishLazycodex
} }
process.env.OMO_INVOCATION_NAME = originalInvocationName
}) })
test("defaults platform to codex when invoked as lazycodex without --platform", () => { test("leaves lazycodex invocation unresolved when lazycodex publishing is disabled", () => {
// given // given
process.env.OMO_INVOCATION_NAME = "lazycodex" 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 // when
const args = resolveInstallArgs({ const args = resolveInstallArgs({
@@ -35,9 +63,10 @@ describe("lazycodex install routing", () => {
expect(config.hasOpenCode).toBe(false) expect(config.hasOpenCode).toBe(false)
}) })
test("respects explicit --platform=both when invoked as lazycodex", () => { test("respects explicit --platform=both when lazycodex publishing is enabled", () => {
// given // given
process.env.OMO_INVOCATION_NAME = "lazycodex" process.env.OMO_INVOCATION_NAME = "lazycodex"
process.env.OMO_PUBLISH_LAZYCODEX = "true"
// when // when
const args = resolveInstallArgs({ const args = resolveInstallArgs({
+12 -1
View File
@@ -48,13 +48,24 @@ describe("install platform resolution", () => {
expect(args.platform).toBe("opencode") expect(args.platform).toBe("opencode")
}) })
test("defaults lazycodex install to codex platform", () => { test("leaves lazycodex install unresolved when lazycodex publishing is disabled", () => {
// given // given
const invocationName = "lazycodex" const invocationName = "lazycodex"
// when // when
const args = resolveInstallArgs({ tui: true }, invocationName) 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 // then
expect(args.platform).toBe("codex") expect(args.platform).toBe("codex")
}) })
+30 -2
View File
@@ -117,24 +117,52 @@ describe("validateNonTuiArgs", () => {
expect(result.errors).toContain("--copilot is required (values: no, yes)") expect(result.errors).toContain("--copilot is required (values: no, yes)")
}) })
test("allows codex-only non-TUI installs without OpenCode provider flags", () => { test("rejects codex-only non-TUI installs when lazycodex publishing is disabled", () => {
// #given // #given
const args: InstallArgs = { tui: false, platform: "codex" } const args: InstallArgs = { tui: false, platform: "codex" }
// #when // #when
const result = validateNonTuiArgs(args) 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 // #then
expect(result.valid).toBe(true) expect(result.valid).toBe(true)
expect(result.errors).toEqual([]) expect(result.errors).toEqual([])
}) })
test("rejects platform=both when lazycodex publishing is disabled", () => {
// #given
const args = createArgs({ platform: "both" })
// #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("rejects OpenCode flags for codex-only non-TUI installs", () => { test("rejects OpenCode flags for codex-only non-TUI installs", () => {
// #given // #given
const args = createArgs({ platform: "codex", claude: "yes" }) const args = createArgs({ platform: "codex", claude: "yes" })
// #when // #when
const result = validateNonTuiArgs(args) const result = validateNonTuiArgs(args, { OMO_PUBLISH_LAZYCODEX: "true" })
// #then // #then
expect(result.valid).toBe(false) expect(result.valid).toBe(false)
+14 -1
View File
@@ -7,6 +7,11 @@ import type {
InstallConfig, InstallConfig,
InstallPlatform, InstallPlatform,
} from "./types" } from "./types"
import {
LAZYCODEX_DISABLED_MESSAGE,
isLazycodexPublishingEnabled,
platformRequiresLazycodex,
} from "./lazycodex-feature-flag"
export const SYMBOLS = { export const SYMBOLS = {
check: color.green("[OK]"), check: color.green("[OK]"),
@@ -19,6 +24,7 @@ export const SYMBOLS = {
} }
const ANSI_COLOR_PATTERN = new RegExp("\u001b\\[[0-9;]*m", "g") 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 { function formatProvider(name: string, enabled: boolean, detail?: string): string {
const status = enabled ? SYMBOLS.check : color.dim("○") const status = enabled ? SYMBOLS.check : color.dim("○")
@@ -116,12 +122,19 @@ export function printBox(content: string, title?: string): void {
console.log() console.log()
} }
export function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors: string[] } { export function validateNonTuiArgs(
args: InstallArgs,
env: Environment = process.env,
): { valid: boolean; errors: string[] } {
const errors: string[] = [] const errors: string[] = []
const platform = resolvePlatform(args) const platform = resolvePlatform(args)
const hasOpenCode = platform === "opencode" || platform === "both" const hasOpenCode = platform === "opencode" || platform === "both"
const hasCodexOnly = platform === "codex" const hasCodexOnly = platform === "codex"
if (platformRequiresLazycodex(platform) && !isLazycodexPublishingEnabled(env)) {
errors.push(LAZYCODEX_DISABLED_MESSAGE)
}
if (hasOpenCode && args.claude === undefined) { if (hasOpenCode && args.claude === undefined) {
errors.push("--claude is required (values: no, yes, max20)") errors.push("--claude is required (values: no, yes, max20)")
} else if (args.claude !== undefined && !["no", "yes", "max20"].includes(args.claude)) { } else if (args.claude !== undefined && !["no", "yes", "max20"].includes(args.claude)) {
+15
View File
@@ -0,0 +1,15 @@
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"
}
+18 -2
View File
@@ -46,12 +46,12 @@ describe("promptInstallPlatform", () => {
mock.restore() mock.restore()
}) })
test("offers OpenCode, Codex, and Both choices with OpenCode as the default", async () => { test("offers OpenCode, Codex, and Both choices when lazycodex publishing is enabled", async () => {
// given // given
const selectSpy = spyOn(p, "select").mockResolvedValue("opencode") const selectSpy = spyOn(p, "select").mockResolvedValue("opencode")
// when // when
const value = await prompts.promptInstallPlatform() const value = await prompts.promptInstallPlatform("opencode", true)
// then // then
expect(value).toBe("opencode") expect(value).toBe("opencode")
@@ -65,6 +65,22 @@ describe("promptInstallPlatform", () => {
], ],
}) })
}) })
test("hides Codex platform choices when lazycodex publishing is disabled", async () => {
// given
const selectSpy = spyOn(p, "select").mockResolvedValue("opencode")
// when
const value = await prompts.promptInstallPlatform("codex", false)
// then
expect(value).toBe("opencode")
expect(selectSpy).toHaveBeenCalledTimes(1)
expect(selectSpy.mock.calls[0]?.[0]).toMatchObject({
initialValue: "opencode",
options: [{ value: "opencode" }],
})
})
}) })
describe("promptInstallConfig platform branching", () => { describe("promptInstallConfig platform branching", () => {
+16 -6
View File
@@ -7,6 +7,7 @@ import type {
InstallPlatform, InstallPlatform,
} from "./types" } from "./types"
import { detectedToInitialValues } from "./install-validators" import { detectedToInitialValues } from "./install-validators"
import { isLazycodexPublishingEnabled } from "./lazycodex-feature-flag"
async function selectOrCancel<TValue extends Readonly<string | boolean | number>>(params: { async function selectOrCancel<TValue extends Readonly<string | boolean | number>>(params: {
message: string message: string
@@ -29,15 +30,24 @@ async function selectOrCancel<TValue extends Readonly<string | boolean | number>
export async function promptInstallPlatform( export async function promptInstallPlatform(
initialValue: InstallPlatform = "opencode", initialValue: InstallPlatform = "opencode",
lazycodexEnabled = isLazycodexPublishingEnabled(),
): Promise<InstallPlatform | null> { ): Promise<InstallPlatform | null> {
return selectOrCancel<InstallPlatform>({ const options: Option<InstallPlatform>[] = [
message: "Which platform do you want to install?", { value: "opencode", label: "OpenCode", hint: "Install OpenCode plugin only" },
options: [ ]
{ value: "opencode", label: "OpenCode", hint: "Install OpenCode plugin only" }, if (lazycodexEnabled) {
options.push(
{ value: "codex", label: "Codex", hint: "Install Codex harness adapter only" }, { value: "codex", label: "Codex", hint: "Install Codex harness adapter only" },
{ value: "both", label: "Both", hint: "Install OpenCode plugin and Codex adapter" }, { value: "both", label: "Both", hint: "Install OpenCode plugin and Codex adapter" },
], )
initialValue, }
const safeInitialValue = lazycodexEnabled || initialValue === "opencode" ? initialValue : "opencode"
return selectOrCancel<InstallPlatform>({
message: "Which platform do you want to install?",
options,
initialValue: safeInitialValue,
}) })
} }
+30
View File
@@ -19,6 +19,7 @@ function createMockSpinner(): ReturnType<typeof p.spinner> {
describe("runTuiInstaller", () => { describe("runTuiInstaller", () => {
const originalIsStdinTty = process.stdin.isTTY const originalIsStdinTty = process.stdin.isTTY
const originalIsStdoutTty = process.stdout.isTTY const originalIsStdoutTty = process.stdout.isTTY
const originalPublishLazycodex = process.env.OMO_PUBLISH_LAZYCODEX
beforeEach(() => { beforeEach(() => {
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true }) Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true })
@@ -28,6 +29,11 @@ describe("runTuiInstaller", () => {
afterEach(() => { afterEach(() => {
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: originalIsStdinTty }) Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: originalIsStdinTty })
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: originalIsStdoutTty }) 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 () => { it("blocks installation when OpenCode is below the minimum version", async () => {
@@ -76,6 +82,29 @@ describe("runTuiInstaller", () => {
outroSpy.mockRestore() 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 () => { it("proceeds when OpenCode meets the minimum version", async () => {
// given // given
const restoreSpies = [ const restoreSpies = [
@@ -143,6 +172,7 @@ describe("runTuiInstaller", () => {
it("skips OpenCode checks and writes when platform is codex", async () => { it("skips OpenCode checks and writes when platform is codex", async () => {
// given // given
process.env.OMO_PUBLISH_LAZYCODEX = "true"
const restoreSpies = [ const restoreSpies = [
spyOn(p, "spinner").mockReturnValue(createMockSpinner()), spyOn(p, "spinner").mockReturnValue(createMockSpinner()),
spyOn(p, "intro").mockImplementation(() => undefined), spyOn(p, "intro").mockImplementation(() => undefined),
+10
View File
@@ -13,6 +13,11 @@ import { detectedToInitialValues, formatConfigSummary, SYMBOLS } from "./install
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version" import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
import { promptInstallConfig, promptInstallPlatform } from "./tui-install-prompts" import { promptInstallConfig, promptInstallPlatform } from "./tui-install-prompts"
import { runCodexInstaller } from "./install-codex" import { runCodexInstaller } from "./install-codex"
import {
LAZYCODEX_DISABLED_MESSAGE,
isLazycodexPublishingEnabled,
platformRequiresLazycodex,
} from "./lazycodex-feature-flag"
export async function runTuiInstaller(args: InstallArgs, version: string): Promise<number> { export async function runTuiInstaller(args: InstallArgs, version: string): Promise<number> {
if (!process.stdin.isTTY || !process.stdout.isTTY) { if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -22,6 +27,11 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
const selectedPlatform = await promptInstallPlatform(args.platform ?? "opencode") const selectedPlatform = await promptInstallPlatform(args.platform ?? "opencode")
if (!selectedPlatform) return 1 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 hasOpenCode = selectedPlatform === "opencode" || selectedPlatform === "both"
const detected = hasOpenCode const detected = hasOpenCode