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
+10 -13
View File
@@ -35,6 +35,9 @@ permissions:
id-token: write
actions: write
env:
LAZYCODEX_VERSION: "0.1.0"
jobs:
test:
runs-on: ubuntu-latest
@@ -312,13 +315,11 @@ jobs:
- name: Check if lazycodex already published
if: inputs.publish_lazycodex == true
id: check-lazycodex
env:
VERSION: ${{ needs.release-metadata.outputs.version }}
run: |
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/lazycodex/${VERSION}")
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/lazycodex/${LAZYCODEX_VERSION}")
if [ "$STATUS" = "200" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "✓ lazycodex@${VERSION} already published"
echo "✓ lazycodex@${LAZYCODEX_VERSION} already published"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
@@ -340,8 +341,6 @@ jobs:
done
jq --arg v "$VERSION" '.optionalDependencies = (.optionalDependencies | to_entries | map(.value = $v) | from_entries)' package.json > tmp.json && mv tmp.json package.json
jq --arg v "$VERSION" '.version = $v' packages/omo-codex/plugin/.codex-plugin/plugin.json > tmp.json && mv tmp.json packages/omo-codex/plugin/.codex-plugin/plugin.json
jq --arg v "$VERSION" '.version = $v' packages/omo-codex/plugin/package.json > tmp.json && mv tmp.json packages/omo-codex/plugin/package.json
- name: Build main package
if: >-
@@ -409,14 +408,14 @@ jobs:
if: inputs.publish_lazycodex == true && steps.check-lazycodex.outputs.skip != 'true'
continue-on-error: true
env:
VERSION: ${{ needs.release-metadata.outputs.version }}
OMO_VERSION: ${{ needs.release-metadata.outputs.version }}
DIST_TAG: ${{ needs.release-metadata.outputs.dist_tag }}
NPM_CONFIG_PROVENANCE: true
run: |
jq --arg v "$VERSION" '
jq --arg lazycodex_version "$LAZYCODEX_VERSION" --arg omo_version "$OMO_VERSION" '
.name = "lazycodex" |
.version = $v |
.optionalDependencies = (.optionalDependencies | to_entries | map(.value = $v) | from_entries)
.version = $lazycodex_version |
.optionalDependencies = (.optionalDependencies | to_entries | map(.value = $omo_version) | from_entries)
' package.json > tmp.json && mv tmp.json package.json
set +e
@@ -526,8 +525,6 @@ jobs:
done
jq --arg v "$VERSION" '.optionalDependencies = (.optionalDependencies | to_entries | map(.value = $v) | from_entries)' package.json > tmp.json && mv tmp.json package.json
jq --arg v "$VERSION" '.version = $v' packages/omo-codex/plugin/.codex-plugin/plugin.json > tmp.json && mv tmp.json packages/omo-codex/plugin/.codex-plugin/plugin.json
jq --arg v "$VERSION" '.version = $v' packages/omo-codex/plugin/package.json > tmp.json && mv tmp.json packages/omo-codex/plugin/package.json
- name: Commit version bump
if: steps.release-state.outputs.release_commit_exists != 'true'
@@ -536,7 +533,7 @@ jobs:
run: |
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
git add package.json packages/oh-my-opencode-*/package.json packages/omo-codex/plugin/.codex-plugin/plugin.json packages/omo-codex/plugin/package.json
git add package.json packages/oh-my-opencode-*/package.json
git diff --cached --quiet || git commit -m "release: v${VERSION}"
- name: Create release tag
+11 -6
View File
@@ -160,7 +160,9 @@ describe("test workflows", () => {
const workflow = readFileSync(publishWorkflowPath, "utf8")
// #when
const appliesCodexPluginVersion = workflow.includes("packages/omo-codex/plugin/.codex-plugin/plugin.json")
const keepsCodexPluginVersionIndependent =
!workflow.includes("jq --arg v \"$VERSION\" '.version = $v' packages/omo-codex/plugin/.codex-plugin/plugin.json") &&
!workflow.includes("jq --arg v \"$VERSION\" '.version = $v' packages/omo-codex/plugin/package.json")
const flagDefaultsOff = workflow.includes("publish_lazycodex:") &&
workflow.includes('description: "Publish lazycodex npm alias and sync Codex marketplace"') &&
workflow.includes("default: false")
@@ -184,7 +186,7 @@ describe("test workflows", () => {
tokenRequirementBeforePublish
// #then
expect(appliesCodexPluginVersion, "release must version the Codex plugin manifest before marketplace sync").toBe(true)
expect(keepsCodexPluginVersionIndependent, "LazyCodex plugin metadata must keep its own 0.1.0 version").toBe(true)
expect(flagDefaultsOff, "LazyCodex deployment must default to disabled").toBe(true)
expect(syncsLazycodexMarketplace, "release must sync the LazyCodex marketplace bundle").toBe(true)
expect(syncBuildsMcpDists, "release must build bundled MCP dists before LazyCodex marketplace sync").toBe(true)
@@ -201,15 +203,18 @@ describe("test workflows", () => {
const platformResolver = readFileSync(new URL("../bin/platform.js", import.meta.url), "utf8")
// #when
const lazycodexStepOnlyRenamesWrapper = workflow.includes('.name = "lazycodex" |') &&
workflow.includes('.version = $v |') &&
workflow.includes('.optionalDependencies = (.optionalDependencies | to_entries | map(.value = $v) | from_entries)')
const lazycodexStepPinsWrapperVersion = workflow.includes('LAZYCODEX_VERSION: "0.1.0"') &&
workflow.includes(".version = $lazycodex_version |")
const lazycodexStepUsesOmoPlatformVersion = workflow.includes(
".optionalDependencies = (.optionalDependencies | to_entries | map(.value = $omo_version) | from_entries)",
)
const lazycodexStepDoesNotRenameOptionalDeps = !workflow.includes('sub("^oh-my-opencode-"; "lazycodex-")')
const shimMapsLazycodexToPublishedPlatformFamily =
platformResolver.includes("lazycodex") && platformResolver.includes("oh-my-opencode")
// #then
expect(lazycodexStepOnlyRenamesWrapper, "lazycodex publish step should only rename wrapper metadata").toBe(true)
expect(lazycodexStepPinsWrapperVersion, "lazycodex publish step must pin wrapper metadata to 0.1.0").toBe(true)
expect(lazycodexStepUsesOmoPlatformVersion, "lazycodex must depend on the matching OMO platform packages").toBe(true)
expect(lazycodexStepDoesNotRenameOptionalDeps, "lazycodex publish step must keep optionalDependencies on published platform packages").toBe(true)
expect(shimMapsLazycodexToPublishedPlatformFamily, "platform resolver must map lazycodex to the real published platform package family").toBe(true)
})
-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