test(cli): batch 82 (10 files)
This commit is contained in:
@@ -18,6 +18,7 @@ describe("runCliInstaller telemetry isolation", () => {
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasCodex: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"
|
||||
import * as configManager from "./config-manager"
|
||||
import * as codexInstaller from "./install-codex"
|
||||
import { runCliInstaller } from "./cli-installer"
|
||||
import type { InstallArgs } from "./types"
|
||||
|
||||
@@ -8,6 +9,7 @@ describe("runCliInstaller", () => {
|
||||
const mockConsoleError = mock(() => {})
|
||||
const originalConsoleLog = console.log
|
||||
const originalConsoleError = console.error
|
||||
const originalPublishLazycodex = process.env.OMO_PUBLISH_LAZYCODEX
|
||||
|
||||
beforeEach(() => {
|
||||
console.log = mockConsoleLog
|
||||
@@ -19,6 +21,11 @@ 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()
|
||||
})
|
||||
|
||||
@@ -33,6 +40,7 @@ describe("runCliInstaller", () => {
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasCodex: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
@@ -46,6 +54,7 @@ describe("runCliInstaller", () => {
|
||||
|
||||
const args: InstallArgs = {
|
||||
tui: false,
|
||||
platform: "opencode",
|
||||
claude: "no",
|
||||
openai: "no",
|
||||
gemini: "no",
|
||||
@@ -80,6 +89,7 @@ describe("runCliInstaller", () => {
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasCodex: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
@@ -100,6 +110,7 @@ describe("runCliInstaller", () => {
|
||||
|
||||
const args: InstallArgs = {
|
||||
tui: false,
|
||||
platform: "opencode",
|
||||
claude: "no",
|
||||
openai: "yes",
|
||||
gemini: "no",
|
||||
@@ -120,4 +131,44 @@ describe("runCliInstaller", () => {
|
||||
spy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
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")
|
||||
const addPluginSpy = spyOn(configManager, "addPluginToOpenCodeConfig")
|
||||
const writeConfigSpy = spyOn(configManager, "writeOmoConfig")
|
||||
|
||||
const codexSpy = spyOn(codexInstaller, "runCodexInstaller").mockResolvedValue({
|
||||
installed: [],
|
||||
configPath: "/tmp/codex-config.toml",
|
||||
codexHome: "/tmp/codex-home",
|
||||
marketplaceName: "sisyphuslabs",
|
||||
})
|
||||
|
||||
const args: InstallArgs = {
|
||||
tui: false,
|
||||
platform: "codex",
|
||||
}
|
||||
|
||||
// when
|
||||
const result = await runCliInstaller(args, "3.4.0")
|
||||
|
||||
// then
|
||||
expect(result).toBe(0)
|
||||
expect(detectSpy).not.toHaveBeenCalled()
|
||||
expect(installedSpy).not.toHaveBeenCalled()
|
||||
expect(versionSpy).not.toHaveBeenCalled()
|
||||
expect(addPluginSpy).not.toHaveBeenCalled()
|
||||
expect(writeConfigSpy).not.toHaveBeenCalled()
|
||||
|
||||
detectSpy.mockRestore()
|
||||
installedSpy.mockRestore()
|
||||
versionSpy.mockRestore()
|
||||
addPluginSpy.mockRestore()
|
||||
writeConfigSpy.mockRestore()
|
||||
codexSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
+79
-39
@@ -23,6 +23,8 @@ import {
|
||||
validateNonTuiArgs,
|
||||
} from "./install-validators"
|
||||
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
||||
import { runCodexInstaller } from "./install-codex"
|
||||
import { STAR_REPOSITORIES, formatGitHubStarCommand } from "./star-request"
|
||||
|
||||
export async function runCliInstaller(args: InstallArgs, version: string): Promise<number> {
|
||||
const validation = validateNonTuiArgs(args)
|
||||
@@ -40,29 +42,49 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
return 1
|
||||
}
|
||||
|
||||
const detected = detectCurrentConfig()
|
||||
const isUpdate = detected.isInstalled
|
||||
const config = argsToConfig(args)
|
||||
const hasOpenCode = config.hasOpenCode
|
||||
const detected = hasOpenCode
|
||||
? detectCurrentConfig()
|
||||
: {
|
||||
isInstalled: false,
|
||||
installedVersion: null,
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasCodex: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
hasOpencodeGo: false,
|
||||
hasVercelAiGateway: false,
|
||||
}
|
||||
const isUpdate = hasOpenCode && detected.isInstalled
|
||||
|
||||
printHeader(isUpdate)
|
||||
|
||||
const totalSteps = 4
|
||||
const totalSteps = hasOpenCode ? 4 : 2
|
||||
let step = 1
|
||||
|
||||
printStep(step++, totalSteps, "Checking OpenCode installation...")
|
||||
const installed = await isOpenCodeInstalled()
|
||||
const openCodeVersion = await getOpenCodeVersion()
|
||||
if (!installed) {
|
||||
printWarning(
|
||||
"OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.",
|
||||
)
|
||||
printInfo("Visit https://opencode.ai/docs for installation instructions")
|
||||
} else {
|
||||
printSuccess(`OpenCode ${openCodeVersion ?? ""} detected`)
|
||||
if (hasOpenCode) {
|
||||
printStep(step++, totalSteps, "Checking OpenCode installation...")
|
||||
const installed = await isOpenCodeInstalled()
|
||||
const openCodeVersion = await getOpenCodeVersion()
|
||||
if (!installed) {
|
||||
printWarning(
|
||||
"OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.",
|
||||
)
|
||||
printInfo("Visit https://opencode.ai/docs for installation instructions")
|
||||
} else {
|
||||
printSuccess(`OpenCode ${openCodeVersion ?? ""} detected`)
|
||||
|
||||
const unsupportedVersionMessage = getUnsupportedOpenCodeVersionMessage(openCodeVersion)
|
||||
if (unsupportedVersionMessage) {
|
||||
printWarning(unsupportedVersionMessage)
|
||||
return 1
|
||||
const unsupportedVersionMessage = getUnsupportedOpenCodeVersionMessage(openCodeVersion)
|
||||
if (unsupportedVersionMessage) {
|
||||
printWarning(unsupportedVersionMessage)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,25 +93,25 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
printInfo(`Current config: Claude=${initial.claude}, Gemini=${initial.gemini}`)
|
||||
}
|
||||
|
||||
const config = argsToConfig(args)
|
||||
if (hasOpenCode) {
|
||||
printStep(step++, totalSteps, `Adding ${PLUGIN_NAME} plugin...`)
|
||||
const pluginResult = await addPluginToOpenCodeConfig(version)
|
||||
if (!pluginResult.success) {
|
||||
printError(`Failed: ${pluginResult.error}`)
|
||||
return 1
|
||||
}
|
||||
printSuccess(
|
||||
`Plugin ${isUpdate ? "verified" : "added"} ${SYMBOLS.arrow} ${color.dim(pluginResult.configPath)}`,
|
||||
)
|
||||
|
||||
printStep(step++, totalSteps, `Adding ${PLUGIN_NAME} plugin...`)
|
||||
const pluginResult = await addPluginToOpenCodeConfig(version)
|
||||
if (!pluginResult.success) {
|
||||
printError(`Failed: ${pluginResult.error}`)
|
||||
return 1
|
||||
printStep(step++, totalSteps, `Writing ${PLUGIN_NAME} configuration...`)
|
||||
const omoResult = writeOmoConfig(config)
|
||||
if (!omoResult.success) {
|
||||
printError(`Failed: ${omoResult.error}`)
|
||||
return 1
|
||||
}
|
||||
printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`)
|
||||
}
|
||||
printSuccess(
|
||||
`Plugin ${isUpdate ? "verified" : "added"} ${SYMBOLS.arrow} ${color.dim(pluginResult.configPath)}`,
|
||||
)
|
||||
|
||||
printStep(step++, totalSteps, `Writing ${PLUGIN_NAME} configuration...`)
|
||||
const omoResult = writeOmoConfig(config)
|
||||
if (!omoResult.success) {
|
||||
printError(`Failed: ${omoResult.error}`)
|
||||
return 1
|
||||
}
|
||||
printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`)
|
||||
|
||||
printBox(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
|
||||
|
||||
@@ -112,9 +134,27 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
}
|
||||
|
||||
console.log(`${SYMBOLS.star} ${color.bold(color.green(isUpdate ? "Configuration updated!" : "Installation complete!"))}`)
|
||||
console.log(` Run ${color.cyan("opencode")} to start!`)
|
||||
if (hasOpenCode) {
|
||||
console.log(` Run ${color.cyan("opencode")} to start!`)
|
||||
}
|
||||
console.log()
|
||||
|
||||
if (config.hasCodex) {
|
||||
printInfo("Installing Codex harness adapter...")
|
||||
try {
|
||||
const codexResult = await runCodexInstaller({ autonomousPermissions: config.codexAutonomous })
|
||||
printSuccess(`Codex plugin installed ${SYMBOLS.arrow} ${color.dim(codexResult.configPath)}`)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
if (!config.hasOpenCode) {
|
||||
printError(`Codex install failed: ${message}`)
|
||||
return 1
|
||||
}
|
||||
printWarning(`Codex install failed (OpenCode install is still complete): ${message}`)
|
||||
}
|
||||
console.log()
|
||||
}
|
||||
|
||||
printInfo(
|
||||
"Anonymous telemetry is enabled by default. Disable it with OMO_SEND_ANONYMOUS_TELEMETRY=0 or OMO_DISABLE_POSTHOG=1.",
|
||||
)
|
||||
@@ -129,14 +169,14 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
)
|
||||
|
||||
console.log(`${SYMBOLS.star} ${color.yellow("If you found this helpful, consider starring the repo!")}`)
|
||||
console.log(
|
||||
` ${color.dim("gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-openagent >/dev/null 2>&1 || true")}`,
|
||||
)
|
||||
for (const repository of STAR_REPOSITORIES) {
|
||||
console.log(` ${color.dim(formatGitHubStarCommand(repository))}`)
|
||||
}
|
||||
console.log()
|
||||
console.log(color.dim("oMoMoMoMo... Enjoy!"))
|
||||
console.log()
|
||||
|
||||
if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
if (hasOpenCode && (config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
printBox(
|
||||
`Run ${color.cyan("opencode auth login")} and select your provider:\n` +
|
||||
(config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") +
|
||||
|
||||
+52
-16
@@ -1,4 +1,4 @@
|
||||
import { Command } from "commander"
|
||||
import { Command, Option } from "commander"
|
||||
import { install } from "./install"
|
||||
import { run } from "./run"
|
||||
import { getLocalVersion } from "./get-local-version"
|
||||
@@ -6,6 +6,7 @@ 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"
|
||||
@@ -16,6 +17,48 @@ const VERSION = packageJson.version
|
||||
|
||||
const program = new Command()
|
||||
|
||||
type InstallCommandOptions = {
|
||||
readonly tui?: boolean
|
||||
readonly claude?: InstallArgs["claude"]
|
||||
readonly openai?: InstallArgs["openai"]
|
||||
readonly gemini?: InstallArgs["gemini"]
|
||||
readonly copilot?: InstallArgs["copilot"]
|
||||
readonly platform?: InstallArgs["platform"]
|
||||
readonly opencodeZen?: InstallArgs["opencodeZen"]
|
||||
readonly zaiCodingPlan?: InstallArgs["zaiCodingPlan"]
|
||||
readonly kimiForCoding?: InstallArgs["kimiForCoding"]
|
||||
readonly opencodeGo?: InstallArgs["opencodeGo"]
|
||||
readonly vercelAiGateway?: InstallArgs["vercelAiGateway"]
|
||||
readonly codexAutonomous?: InstallArgs["codexAutonomous"]
|
||||
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
|
||||
|
||||
return {
|
||||
tui: options.tui !== false,
|
||||
claude: options.claude,
|
||||
openai: options.openai,
|
||||
gemini: options.gemini,
|
||||
copilot: options.copilot,
|
||||
platform: options.platform ?? defaultPlatform,
|
||||
opencodeZen: options.opencodeZen,
|
||||
zaiCodingPlan: options.zaiCodingPlan,
|
||||
kimiForCoding: options.kimiForCoding,
|
||||
opencodeGo: options.opencodeGo,
|
||||
vercelAiGateway: options.vercelAiGateway,
|
||||
codexAutonomous: options.codexAutonomous,
|
||||
skipAuth: options.skipAuth ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
.name("oh-my-opencode")
|
||||
.description("The ultimate OpenCode plugin - multi-model orchestration, LSP tools, and more")
|
||||
@@ -32,16 +75,21 @@ program
|
||||
.option("--openai <value>", "OpenAI/ChatGPT subscription: no, yes (default: no)")
|
||||
.option("--gemini <value>", "Gemini integration: no, yes")
|
||||
.option("--copilot <value>", "GitHub Copilot subscription: no, yes")
|
||||
.addOption(new Option("--platform <platform>", "Install target platform: opencode, codex, both").choices(["opencode", "codex", "both"]))
|
||||
.option("--opencode-zen <value>", "OpenCode Zen access: no, yes (default: no)")
|
||||
.option("--zai-coding-plan <value>", "Z.ai Coding Plan subscription: no, yes (default: no)")
|
||||
.option("--kimi-for-coding <value>", "Kimi For Coding subscription: no, yes (default: no)")
|
||||
.option("--opencode-go <value>", "OpenCode Go subscription: no, yes (default: no)")
|
||||
.option("--vercel-ai-gateway <value>", "Vercel AI Gateway: no, yes (default: no)")
|
||||
.option("--codex-autonomous", "Configure Codex with approval never, full filesystem access, and network enabled")
|
||||
.option("--no-codex-autonomous", "Leave existing Codex permission settings unchanged")
|
||||
.option("--skip-auth", "Skip authentication setup hints")
|
||||
.addHelpText("after", `
|
||||
.addHelpText("after", `
|
||||
Examples:
|
||||
$ bunx oh-my-opencode install
|
||||
$ bunx oh-my-opencode install --no-tui --claude=max20 --openai=yes --gemini=yes --copilot=no
|
||||
$ bunx lazycodex install --no-tui
|
||||
$ bunx oh-my-opencode install --no-tui --platform=both --claude=max20 --openai=yes --gemini=yes --copilot=no
|
||||
$ omo install --platform=codex --codex-autonomous
|
||||
$ bunx oh-my-opencode install --no-tui --claude=no --gemini=no --copilot=yes --opencode-zen=yes
|
||||
|
||||
Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi > Vercel):
|
||||
@@ -55,19 +103,7 @@ Model Providers (Priority: Native > Copilot > OpenCode Zen > Z.ai > Kimi > Verce
|
||||
Vercel vercel/ models (universal proxy, always last fallback)
|
||||
`)
|
||||
.action(async (options) => {
|
||||
const args: InstallArgs = {
|
||||
tui: options.tui !== false,
|
||||
claude: options.claude,
|
||||
openai: options.openai,
|
||||
gemini: options.gemini,
|
||||
copilot: options.copilot,
|
||||
opencodeZen: options.opencodeZen,
|
||||
zaiCodingPlan: options.zaiCodingPlan,
|
||||
kimiForCoding: options.kimiForCoding,
|
||||
opencodeGo: options.opencodeGo,
|
||||
vercelAiGateway: options.vercelAiGateway,
|
||||
skipAuth: options.skipAuth ?? false,
|
||||
}
|
||||
const args = resolveInstallArgs(options)
|
||||
const exitCode = await install(args)
|
||||
process.exit(exitCode)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
import { validateNonTuiArgs } from "./install-validators"
|
||||
import { argsToConfig, formatConfigSummary, validateNonTuiArgs } from "./install-validators"
|
||||
import type { InstallArgs } from "./types"
|
||||
|
||||
function createArgs(overrides: Partial<InstallArgs> = {}): InstallArgs {
|
||||
@@ -14,11 +16,79 @@ function createArgs(overrides: Partial<InstallArgs> = {}): InstallArgs {
|
||||
zaiCodingPlan: "no",
|
||||
kimiForCoding: "no",
|
||||
opencodeGo: "no",
|
||||
vercelAiGateway: "no",
|
||||
skipAuth: false,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe("argsToConfig", () => {
|
||||
test("enables only OpenCode when platform is opencode", () => {
|
||||
// #given
|
||||
const args = createArgs({ platform: "opencode" })
|
||||
|
||||
// #when
|
||||
const config = argsToConfig(args)
|
||||
|
||||
// #then
|
||||
expect(config.platform).toBe("opencode")
|
||||
expect(config.hasOpenCode).toBe(true)
|
||||
expect(config.hasCodex).toBe(false)
|
||||
})
|
||||
|
||||
test("enables only Codex when platform is codex", () => {
|
||||
// #given
|
||||
const args = createArgs({ platform: "codex", codexAutonomous: true })
|
||||
|
||||
// #when
|
||||
const config = argsToConfig(args)
|
||||
|
||||
// #then
|
||||
expect(config.platform).toBe("codex")
|
||||
expect(config.hasOpenCode).toBe(false)
|
||||
expect(config.hasCodex).toBe(true)
|
||||
expect(config.codexAutonomous).toBe(true)
|
||||
})
|
||||
|
||||
test("ignores Codex autonomous mode when Codex is not installed", () => {
|
||||
// #given
|
||||
const args = createArgs({ platform: "opencode", codexAutonomous: true })
|
||||
|
||||
// #when
|
||||
const config = argsToConfig(args)
|
||||
|
||||
// #then
|
||||
expect(config.hasCodex).toBe(false)
|
||||
expect(config.codexAutonomous).toBe(false)
|
||||
})
|
||||
|
||||
test("enables both harnesses when platform is both", () => {
|
||||
// #given
|
||||
const args = createArgs({ platform: "both" })
|
||||
|
||||
// #when
|
||||
const config = argsToConfig(args)
|
||||
|
||||
// #then
|
||||
expect(config.platform).toBe("both")
|
||||
expect(config.hasOpenCode).toBe(true)
|
||||
expect(config.hasCodex).toBe(true)
|
||||
})
|
||||
|
||||
test("defaults to OpenCode when platform is omitted", () => {
|
||||
// #given
|
||||
const args = createArgs()
|
||||
|
||||
// #when
|
||||
const config = argsToConfig(args)
|
||||
|
||||
// #then
|
||||
expect(config.platform).toBe("opencode")
|
||||
expect(config.hasOpenCode).toBe(true)
|
||||
expect(config.hasCodex).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("validateNonTuiArgs", () => {
|
||||
test("rejects invalid --opencode-go values", () => {
|
||||
// #given
|
||||
@@ -31,4 +101,98 @@ describe("validateNonTuiArgs", () => {
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain("Invalid --opencode-go value: maybe (expected: no, yes)")
|
||||
})
|
||||
|
||||
test("requires OpenCode provider flags when platform is opencode", () => {
|
||||
// #given
|
||||
const args = createArgs({ platform: "opencode", claude: undefined, gemini: undefined, copilot: undefined })
|
||||
|
||||
// #when
|
||||
const result = validateNonTuiArgs(args)
|
||||
|
||||
// #then
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain("--claude is required (values: no, yes, max20)")
|
||||
expect(result.errors).toContain("--gemini is required (values: no, yes)")
|
||||
expect(result.errors).toContain("--copilot is required (values: no, yes)")
|
||||
})
|
||||
|
||||
test("requires OpenCode provider flags when platform is both", () => {
|
||||
// #given
|
||||
const args = createArgs({ platform: "both", claude: undefined, gemini: undefined, copilot: undefined })
|
||||
|
||||
// #when
|
||||
const result = validateNonTuiArgs(args)
|
||||
|
||||
// #then
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain("--claude is required (values: no, yes, max20)")
|
||||
expect(result.errors).toContain("--gemini is required (values: no, yes)")
|
||||
expect(result.errors).toContain("--copilot is required (values: no, yes)")
|
||||
})
|
||||
|
||||
test("rejects codex-only non-TUI installs when lazycodex publishing is disabled", () => {
|
||||
// #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", () => {
|
||||
// #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", () => {
|
||||
// #given
|
||||
const args = createArgs({ platform: "codex", claude: "yes" })
|
||||
|
||||
// #when
|
||||
const result = validateNonTuiArgs(args, { OMO_PUBLISH_LAZYCODEX: "true" })
|
||||
|
||||
// #then
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain("--claude cannot be used with --platform=codex")
|
||||
})
|
||||
})
|
||||
|
||||
describe("formatConfigSummary", () => {
|
||||
test("shows platform instead of a separate Codex Harness provider line", () => {
|
||||
// #given
|
||||
const config = argsToConfig(createArgs({ platform: "both" }))
|
||||
|
||||
// #when
|
||||
const summary = formatConfigSummary(config)
|
||||
|
||||
// #then
|
||||
expect(summary).toContain("Platform: both")
|
||||
expect(summary).not.toContain("Codex Harness")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,13 @@ import type {
|
||||
DetectedConfig,
|
||||
InstallArgs,
|
||||
InstallConfig,
|
||||
InstallPlatform,
|
||||
} from "./types"
|
||||
import {
|
||||
LAZYCODEX_DISABLED_MESSAGE,
|
||||
isLazycodexPublishingEnabled,
|
||||
platformRequiresLazycodex,
|
||||
} from "./lazycodex-feature-flag"
|
||||
|
||||
export const SYMBOLS = {
|
||||
check: color.green("[OK]"),
|
||||
@@ -18,6 +24,7 @@ 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("○")
|
||||
@@ -31,6 +38,11 @@ export function formatConfigSummary(config: InstallConfig): string {
|
||||
|
||||
lines.push(color.bold(color.white("Configuration Summary")))
|
||||
lines.push("")
|
||||
lines.push(` ${SYMBOLS.info} Platform: ${config.platform}`)
|
||||
if (config.hasCodex) {
|
||||
lines.push(` ${SYMBOLS.info} Codex autonomous mode: ${config.codexAutonomous ? "enabled" : "disabled"}`)
|
||||
}
|
||||
lines.push("")
|
||||
|
||||
const claudeDetail = config.hasClaude ? (config.isMax20 ? "max20" : "standard") : undefined
|
||||
lines.push(formatProvider("Claude", config.hasClaude, claudeDetail))
|
||||
@@ -113,24 +125,34 @@ export function printBox(content: string, title?: string): void {
|
||||
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 platform = resolvePlatform(args)
|
||||
const hasOpenCode = platform === "opencode" || platform === "both"
|
||||
const hasCodexOnly = platform === "codex"
|
||||
|
||||
if (args.claude === undefined) {
|
||||
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 (!["no", "yes", "max20"].includes(args.claude)) {
|
||||
} else if (args.claude !== undefined && !["no", "yes", "max20"].includes(args.claude)) {
|
||||
errors.push(`Invalid --claude value: ${args.claude} (expected: no, yes, max20)`)
|
||||
}
|
||||
|
||||
if (args.gemini === undefined) {
|
||||
if (hasOpenCode && args.gemini === undefined) {
|
||||
errors.push("--gemini is required (values: no, yes)")
|
||||
} else if (!["no", "yes"].includes(args.gemini)) {
|
||||
} else if (args.gemini !== undefined && !["no", "yes"].includes(args.gemini)) {
|
||||
errors.push(`Invalid --gemini value: ${args.gemini} (expected: no, yes)`)
|
||||
}
|
||||
|
||||
if (args.copilot === undefined) {
|
||||
if (hasOpenCode && args.copilot === undefined) {
|
||||
errors.push("--copilot is required (values: no, yes)")
|
||||
} else if (!["no", "yes"].includes(args.copilot)) {
|
||||
} else if (args.copilot !== undefined && !["no", "yes"].includes(args.copilot)) {
|
||||
errors.push(`Invalid --copilot value: ${args.copilot} (expected: no, yes)`)
|
||||
}
|
||||
|
||||
@@ -158,21 +180,52 @@ export function validateNonTuiArgs(args: InstallArgs): { valid: boolean; errors:
|
||||
errors.push(`Invalid --vercel-ai-gateway value: ${args.vercelAiGateway} (expected: no, yes)`)
|
||||
}
|
||||
|
||||
if (hasCodexOnly) {
|
||||
const opencodeFlagErrors = collectCodexOnlyOpenCodeFlagErrors(args)
|
||||
errors.push(...opencodeFlagErrors)
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors }
|
||||
}
|
||||
|
||||
function resolvePlatform(args: InstallArgs): InstallPlatform {
|
||||
return args.platform ?? "opencode"
|
||||
}
|
||||
|
||||
function collectCodexOnlyOpenCodeFlagErrors(args: InstallArgs): string[] {
|
||||
const errors: string[] = []
|
||||
if (args.claude !== undefined) errors.push("--claude cannot be used with --platform=codex")
|
||||
if (args.openai !== undefined) errors.push("--openai cannot be used with --platform=codex")
|
||||
if (args.gemini !== undefined) errors.push("--gemini cannot be used with --platform=codex")
|
||||
if (args.copilot !== undefined) errors.push("--copilot cannot be used with --platform=codex")
|
||||
if (args.opencodeZen !== undefined) errors.push("--opencode-zen cannot be used with --platform=codex")
|
||||
if (args.zaiCodingPlan !== undefined) errors.push("--zai-coding-plan cannot be used with --platform=codex")
|
||||
if (args.kimiForCoding !== undefined) errors.push("--kimi-for-coding cannot be used with --platform=codex")
|
||||
if (args.opencodeGo !== undefined) errors.push("--opencode-go cannot be used with --platform=codex")
|
||||
if (args.vercelAiGateway !== undefined) errors.push("--vercel-ai-gateway cannot be used with --platform=codex")
|
||||
return errors
|
||||
}
|
||||
|
||||
export function argsToConfig(args: InstallArgs): InstallConfig {
|
||||
const platform = resolvePlatform(args)
|
||||
const hasOpenCode = platform === "opencode" || platform === "both"
|
||||
const hasCodex = platform === "codex" || platform === "both"
|
||||
|
||||
return {
|
||||
hasClaude: args.claude !== "no",
|
||||
platform,
|
||||
hasOpenCode,
|
||||
hasClaude: hasOpenCode && args.claude !== "no",
|
||||
isMax20: args.claude === "max20",
|
||||
hasOpenAI: args.openai === "yes",
|
||||
hasGemini: args.gemini === "yes",
|
||||
hasCopilot: args.copilot === "yes",
|
||||
hasOpencodeZen: args.opencodeZen === "yes",
|
||||
hasZaiCodingPlan: args.zaiCodingPlan === "yes",
|
||||
hasKimiForCoding: args.kimiForCoding === "yes",
|
||||
hasOpencodeGo: args.opencodeGo === "yes",
|
||||
hasVercelAiGateway: args.vercelAiGateway === "yes",
|
||||
hasOpenAI: hasOpenCode && args.openai === "yes",
|
||||
hasGemini: hasOpenCode && args.gemini === "yes",
|
||||
hasCopilot: hasOpenCode && args.copilot === "yes",
|
||||
hasCodex,
|
||||
hasOpencodeZen: hasOpenCode && args.opencodeZen === "yes",
|
||||
hasZaiCodingPlan: hasOpenCode && args.zaiCodingPlan === "yes",
|
||||
hasKimiForCoding: hasOpenCode && args.kimiForCoding === "yes",
|
||||
hasOpencodeGo: hasOpenCode && args.opencodeGo === "yes",
|
||||
hasVercelAiGateway: hasOpenCode && args.vercelAiGateway === "yes",
|
||||
codexAutonomous: hasCodex && args.codexAutonomous === true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +236,7 @@ export function detectedToInitialValues(detected: DetectedConfig): {
|
||||
copilot: BooleanArg
|
||||
opencodeZen: BooleanArg
|
||||
zaiCodingPlan: BooleanArg
|
||||
kimiForCoding: BooleanArg
|
||||
kimiForCoding: BooleanArg
|
||||
opencodeGo: BooleanArg
|
||||
vercelAiGateway: BooleanArg
|
||||
} {
|
||||
@@ -199,7 +252,7 @@ kimiForCoding: BooleanArg
|
||||
copilot: detected.hasCopilot ? "yes" : "no",
|
||||
opencodeZen: detected.hasOpencodeZen ? "yes" : "no",
|
||||
zaiCodingPlan: detected.hasZaiCodingPlan ? "yes" : "no",
|
||||
kimiForCoding: detected.hasKimiForCoding ? "yes" : "no",
|
||||
kimiForCoding: detected.hasKimiForCoding ? "yes" : "no",
|
||||
opencodeGo: detected.hasOpencodeGo ? "yes" : "no",
|
||||
vercelAiGateway: detected.hasVercelAiGateway ? "yes" : "no",
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import type {
|
||||
ClaudeSubscription,
|
||||
DetectedConfig,
|
||||
InstallConfig,
|
||||
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
|
||||
@@ -26,7 +28,58 @@ async function selectOrCancel<TValue extends Readonly<string | boolean | number>
|
||||
return value as TValue
|
||||
}
|
||||
|
||||
export async function promptInstallConfig(detected: DetectedConfig): Promise<InstallConfig | null> {
|
||||
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" },
|
||||
]
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
export async function promptInstallConfig(
|
||||
detected: DetectedConfig,
|
||||
platform: InstallPlatform,
|
||||
codexAutonomousOverride?: boolean,
|
||||
): Promise<InstallConfig | null> {
|
||||
const hasOpenCode = platform === "opencode" || platform === "both"
|
||||
const hasCodex = platform === "codex" || platform === "both"
|
||||
const codexAutonomous = await resolveCodexAutonomous(hasCodex, codexAutonomousOverride)
|
||||
if (codexAutonomous === null) return null
|
||||
|
||||
if (!hasOpenCode) {
|
||||
return {
|
||||
platform,
|
||||
hasOpenCode: false,
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasCodex,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
hasOpencodeGo: false,
|
||||
hasVercelAiGateway: false,
|
||||
codexAutonomous,
|
||||
}
|
||||
}
|
||||
|
||||
const initial = detectedToInitialValues(detected)
|
||||
|
||||
const claude = await selectOrCancel<ClaudeSubscription>({
|
||||
@@ -121,15 +174,36 @@ export async function promptInstallConfig(detected: DetectedConfig): Promise<Ins
|
||||
if (!vercelAiGateway) return null
|
||||
|
||||
return {
|
||||
platform,
|
||||
hasOpenCode: true,
|
||||
hasClaude: claude !== "no",
|
||||
isMax20: claude === "max20",
|
||||
hasOpenAI: openai === "yes",
|
||||
hasGemini: gemini === "yes",
|
||||
hasCopilot: copilot === "yes",
|
||||
hasCodex,
|
||||
hasOpencodeZen: opencodeZen === "yes",
|
||||
hasZaiCodingPlan: zaiCodingPlan === "yes",
|
||||
hasKimiForCoding: kimiForCoding === "yes",
|
||||
hasOpencodeGo: opencodeGo === "yes",
|
||||
hasVercelAiGateway: vercelAiGateway === "yes",
|
||||
codexAutonomous,
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveCodexAutonomous(
|
||||
hasCodex: boolean,
|
||||
override: boolean | undefined,
|
||||
): Promise<boolean | null> {
|
||||
if (!hasCodex) return false
|
||||
if (override !== undefined) return override
|
||||
|
||||
return selectOrCancel<boolean>({
|
||||
message: "Configure Codex for autonomous full-permissions mode?",
|
||||
options: [
|
||||
{ value: true, label: "Yes", hint: "Recommended: approval never, danger-full-access, network enabled" },
|
||||
{ value: false, label: "No", hint: "Leave existing Codex permissions unchanged" },
|
||||
],
|
||||
initialValue: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,12 +9,17 @@ function createMockSpinner(): ReturnType<typeof p.spinner> {
|
||||
start: () => undefined,
|
||||
stop: () => undefined,
|
||||
message: () => undefined,
|
||||
cancel: () => undefined,
|
||||
error: () => undefined,
|
||||
clear: () => undefined,
|
||||
isCancelled: false,
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
@@ -24,6 +29,11 @@ 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 () => {
|
||||
@@ -32,6 +42,7 @@ describe("runTuiInstaller", () => {
|
||||
spyOn(p, "spinner").mockReturnValue(createMockSpinner()),
|
||||
spyOn(p, "intro").mockImplementation(() => undefined),
|
||||
spyOn(p.log, "warn").mockImplementation(() => undefined),
|
||||
spyOn(tuiInstallPrompts, "promptInstallPlatform").mockResolvedValue("opencode"),
|
||||
spyOn(configManager, "detectCurrentConfig").mockReturnValue({
|
||||
isInstalled: false,
|
||||
installedVersion: null,
|
||||
@@ -40,11 +51,13 @@ describe("runTuiInstaller", () => {
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasCodex: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
hasOpencodeGo: false,
|
||||
hasVercelAiGateway: false,
|
||||
codexAutonomous: false,
|
||||
}),
|
||||
spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true),
|
||||
spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.3.9"),
|
||||
@@ -70,6 +83,29 @@ 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 = [
|
||||
@@ -81,6 +117,7 @@ describe("runTuiInstaller", () => {
|
||||
spyOn(p.log, "message").mockImplementation(() => undefined),
|
||||
spyOn(p, "note").mockImplementation(() => undefined),
|
||||
spyOn(p, "outro").mockImplementation(() => undefined),
|
||||
spyOn(tuiInstallPrompts, "promptInstallPlatform").mockResolvedValue("opencode"),
|
||||
spyOn(configManager, "detectCurrentConfig").mockReturnValue({
|
||||
isInstalled: false,
|
||||
installedVersion: null,
|
||||
@@ -89,25 +126,31 @@ describe("runTuiInstaller", () => {
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasCodex: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
hasOpencodeGo: false,
|
||||
hasVercelAiGateway: false,
|
||||
codexAutonomous: false,
|
||||
}),
|
||||
spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true),
|
||||
spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0"),
|
||||
spyOn(tuiInstallPrompts, "promptInstallConfig").mockResolvedValue({
|
||||
platform: "opencode",
|
||||
hasOpenCode: true,
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasCodex: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
hasOpencodeGo: false,
|
||||
hasVercelAiGateway: false,
|
||||
codexAutonomous: false,
|
||||
}),
|
||||
spyOn(configManager, "addPluginToOpenCodeConfig").mockResolvedValue({
|
||||
success: true,
|
||||
@@ -129,4 +172,60 @@ describe("runTuiInstaller", () => {
|
||||
spy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
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),
|
||||
spyOn(p.log, "info").mockImplementation(() => undefined),
|
||||
spyOn(p.log, "warn").mockImplementation(() => undefined),
|
||||
spyOn(p.log, "success").mockImplementation(() => undefined),
|
||||
spyOn(p.log, "message").mockImplementation(() => undefined),
|
||||
spyOn(p, "note").mockImplementation(() => undefined),
|
||||
spyOn(p, "outro").mockImplementation(() => undefined),
|
||||
spyOn(tuiInstallPrompts, "promptInstallPlatform").mockResolvedValue("codex"),
|
||||
spyOn(tuiInstallPrompts, "promptInstallConfig").mockResolvedValue({
|
||||
platform: "codex",
|
||||
hasOpenCode: false,
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasCodex: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
hasOpencodeGo: false,
|
||||
hasVercelAiGateway: false,
|
||||
}),
|
||||
]
|
||||
const detectConfigSpy = spyOn(configManager, "detectCurrentConfig")
|
||||
const isInstalledSpy = spyOn(configManager, "isOpenCodeInstalled")
|
||||
const getVersionSpy = spyOn(configManager, "getOpenCodeVersion")
|
||||
const addPluginSpy = spyOn(configManager, "addPluginToOpenCodeConfig")
|
||||
const writeConfigSpy = spyOn(configManager, "writeOmoConfig")
|
||||
|
||||
// when
|
||||
const result = await runTuiInstaller({ tui: true, platform: "codex" }, "3.16.0")
|
||||
|
||||
// then
|
||||
expect(result).toBe(0)
|
||||
expect(detectConfigSpy).not.toHaveBeenCalled()
|
||||
expect(isInstalledSpy).not.toHaveBeenCalled()
|
||||
expect(getVersionSpy).not.toHaveBeenCalled()
|
||||
expect(addPluginSpy).not.toHaveBeenCalled()
|
||||
expect(writeConfigSpy).not.toHaveBeenCalled()
|
||||
|
||||
for (const spy of restoreSpies) {
|
||||
spy.mockRestore()
|
||||
}
|
||||
detectConfigSpy.mockRestore()
|
||||
isInstalledSpy.mockRestore()
|
||||
getVersionSpy.mockRestore()
|
||||
addPluginSpy.mockRestore()
|
||||
writeConfigSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
+93
-38
@@ -11,7 +11,14 @@ import {
|
||||
} from "./config-manager"
|
||||
import { detectedToInitialValues, formatConfigSummary, SYMBOLS } from "./install-validators"
|
||||
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
||||
import { promptInstallConfig } from "./tui-install-prompts"
|
||||
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> {
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
@@ -19,8 +26,33 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
|
||||
return 1
|
||||
}
|
||||
|
||||
const detected = detectCurrentConfig()
|
||||
const isUpdate = detected.isInstalled
|
||||
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
|
||||
? detectCurrentConfig()
|
||||
: {
|
||||
isInstalled: false,
|
||||
installedVersion: null,
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasCodex: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
hasOpencodeGo: false,
|
||||
hasVercelAiGateway: false,
|
||||
}
|
||||
const isUpdate = hasOpenCode && detected.isInstalled
|
||||
|
||||
p.intro(color.bgMagenta(color.white(isUpdate ? " oMoMoMoMo... Update " : " oMoMoMoMo... ")))
|
||||
|
||||
@@ -30,45 +62,49 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
|
||||
}
|
||||
|
||||
const spinner = p.spinner()
|
||||
spinner.start("Checking OpenCode installation")
|
||||
if (hasOpenCode) {
|
||||
spinner.start("Checking OpenCode installation")
|
||||
|
||||
const installed = await isOpenCodeInstalled()
|
||||
const openCodeVersion = await getOpenCodeVersion()
|
||||
if (!installed) {
|
||||
spinner.stop(`OpenCode binary not found ${color.yellow("[!]")}`)
|
||||
p.log.warn("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.")
|
||||
p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide")
|
||||
} else {
|
||||
spinner.stop(`OpenCode ${openCodeVersion ?? "installed"} ${color.green("[OK]")}`)
|
||||
const installed = await isOpenCodeInstalled()
|
||||
const openCodeVersion = await getOpenCodeVersion()
|
||||
if (!installed) {
|
||||
spinner.stop(`OpenCode binary not found ${color.yellow("[!]")}`)
|
||||
p.log.warn("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.")
|
||||
p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide")
|
||||
} else {
|
||||
spinner.stop(`OpenCode ${openCodeVersion ?? "installed"} ${color.green("[OK]")}`)
|
||||
|
||||
const unsupportedVersionMessage = getUnsupportedOpenCodeVersionMessage(openCodeVersion)
|
||||
if (unsupportedVersionMessage) {
|
||||
p.log.warn(unsupportedVersionMessage)
|
||||
p.outro(color.red("Installation blocked."))
|
||||
return 1
|
||||
const unsupportedVersionMessage = getUnsupportedOpenCodeVersionMessage(openCodeVersion)
|
||||
if (unsupportedVersionMessage) {
|
||||
p.log.warn(unsupportedVersionMessage)
|
||||
p.outro(color.red("Installation blocked."))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = await promptInstallConfig(detected)
|
||||
const config = await promptInstallConfig(detected, selectedPlatform, args.codexAutonomous)
|
||||
if (!config) return 1
|
||||
|
||||
spinner.start(`Adding ${PLUGIN_NAME} to OpenCode config`)
|
||||
const pluginResult = await addPluginToOpenCodeConfig(version)
|
||||
if (!pluginResult.success) {
|
||||
spinner.stop(`Failed to add plugin: ${pluginResult.error}`)
|
||||
p.outro(color.red("Installation failed."))
|
||||
return 1
|
||||
}
|
||||
spinner.stop(`Plugin added to ${color.cyan(pluginResult.configPath)}`)
|
||||
if (config.hasOpenCode) {
|
||||
spinner.start(`Adding ${PLUGIN_NAME} to OpenCode config`)
|
||||
const pluginResult = await addPluginToOpenCodeConfig(version)
|
||||
if (!pluginResult.success) {
|
||||
spinner.stop(`Failed to add plugin: ${pluginResult.error}`)
|
||||
p.outro(color.red("Installation failed."))
|
||||
return 1
|
||||
}
|
||||
spinner.stop(`Plugin added to ${color.cyan(pluginResult.configPath)}`)
|
||||
|
||||
spinner.start(`Writing ${PLUGIN_NAME} configuration`)
|
||||
const omoResult = writeOmoConfig(config)
|
||||
if (!omoResult.success) {
|
||||
spinner.stop(`Failed to write config: ${omoResult.error}`)
|
||||
p.outro(color.red("Installation failed."))
|
||||
return 1
|
||||
spinner.start(`Writing ${PLUGIN_NAME} configuration`)
|
||||
const omoResult = writeOmoConfig(config)
|
||||
if (!omoResult.success) {
|
||||
spinner.stop(`Failed to write config: ${omoResult.error}`)
|
||||
p.outro(color.red("Installation failed."))
|
||||
return 1
|
||||
}
|
||||
spinner.stop(`Config written to ${color.cyan(omoResult.configPath)}`)
|
||||
}
|
||||
spinner.stop(`Config written to ${color.cyan(omoResult.configPath)}`)
|
||||
|
||||
if (!config.hasClaude) {
|
||||
p.log.info(
|
||||
@@ -83,8 +119,27 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
|
||||
|
||||
p.note(formatConfigSummary(config), isUpdate ? "Updated Configuration" : "Installation Complete")
|
||||
|
||||
if (config.hasCodex) {
|
||||
spinner.start("Installing Codex harness adapter")
|
||||
try {
|
||||
const codexResult = await runCodexInstaller({ autonomousPermissions: config.codexAutonomous })
|
||||
spinner.stop(`Codex plugin installed to ${color.cyan(codexResult.configPath)}`)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
spinner.stop(`Codex install failed ${color.yellow("[!]")}`)
|
||||
if (!config.hasOpenCode) {
|
||||
p.log.error(`Codex install failed: ${message}`)
|
||||
p.outro(color.red("Installation failed."))
|
||||
return 1
|
||||
}
|
||||
p.log.warn(`Codex install failed (OpenCode install remains successful): ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
p.log.success(color.bold(isUpdate ? "Configuration updated!" : "Installation complete!"))
|
||||
p.log.message(`Run ${color.cyan("opencode")} to start!`)
|
||||
if (config.hasOpenCode) {
|
||||
p.log.message(`Run ${color.cyan("opencode")} to start!`)
|
||||
}
|
||||
p.log.info("Anonymous telemetry is enabled by default. Disable it with OMO_SEND_ANONYMOUS_TELEMETRY=0 or OMO_DISABLE_POSTHOG=1.")
|
||||
p.log.info("Docs: docs/legal/privacy-policy.md and docs/legal/terms-of-service.md")
|
||||
|
||||
@@ -96,13 +151,13 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
|
||||
)
|
||||
|
||||
p.log.message(`${color.yellow("★")} If you found this helpful, consider starring the repo!`)
|
||||
p.log.message(
|
||||
` ${color.dim("gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-openagent >/dev/null 2>&1 || true")}`,
|
||||
)
|
||||
for (const repository of STAR_REPOSITORIES) {
|
||||
p.log.message(` ${color.dim(formatGitHubStarCommand(repository))}`)
|
||||
}
|
||||
|
||||
p.outro(color.green("oMoMoMoMo... Enjoy!"))
|
||||
|
||||
if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
if (config.hasOpenCode && (config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
const providers: string[] = []
|
||||
if (config.hasClaude) providers.push(`Anthropic ${color.gray("→ Claude Pro/Max")}`)
|
||||
if (config.hasGemini) providers.push(`Google ${color.gray("→ Gemini")}`)
|
||||
|
||||
+9
-1
@@ -1,31 +1,38 @@
|
||||
export type ClaudeSubscription = "no" | "yes" | "max20"
|
||||
export type BooleanArg = "no" | "yes"
|
||||
export type InstallPlatform = "opencode" | "codex" | "both"
|
||||
|
||||
export interface InstallArgs {
|
||||
tui: boolean
|
||||
platform?: InstallPlatform
|
||||
claude?: ClaudeSubscription
|
||||
openai?: BooleanArg
|
||||
gemini?: BooleanArg
|
||||
copilot?: BooleanArg
|
||||
opencodeZen?: BooleanArg
|
||||
zaiCodingPlan?: BooleanArg
|
||||
kimiForCoding?: BooleanArg
|
||||
kimiForCoding?: BooleanArg
|
||||
opencodeGo?: BooleanArg
|
||||
vercelAiGateway?: BooleanArg
|
||||
codexAutonomous?: boolean
|
||||
skipAuth?: boolean
|
||||
}
|
||||
|
||||
export interface InstallConfig {
|
||||
platform: InstallPlatform
|
||||
hasOpenCode: boolean
|
||||
hasClaude: boolean
|
||||
isMax20: boolean
|
||||
hasOpenAI: boolean
|
||||
hasGemini: boolean
|
||||
hasCopilot: boolean
|
||||
hasCodex: boolean
|
||||
hasOpencodeZen: boolean
|
||||
hasZaiCodingPlan: boolean
|
||||
hasKimiForCoding: boolean
|
||||
hasOpencodeGo: boolean
|
||||
hasVercelAiGateway: boolean
|
||||
codexAutonomous: boolean
|
||||
}
|
||||
|
||||
export interface ConfigMergeResult {
|
||||
@@ -42,6 +49,7 @@ export interface DetectedConfig {
|
||||
hasOpenAI: boolean
|
||||
hasGemini: boolean
|
||||
hasCopilot: boolean
|
||||
hasCodex: boolean
|
||||
hasOpencodeZen: boolean
|
||||
hasZaiCodingPlan: boolean
|
||||
hasKimiForCoding: boolean
|
||||
|
||||
Reference in New Issue
Block a user