feat(cli): add claudecode install platform + runner + lazyclaudecode routing
--platform=claudecode (alias cc normalized in resolveInstallArgs); hasClaudeCode; runClaudeCodeInstaller shells to claude plugin marketplace add/install, prints /plugin commands + errors (no ~/.claude write) when claude absent; install telemetry via @oh-my-opencode/omo-claude/telemetry; lazyclaudecode invocation routes to claudecode. 26 target tests + 23 dependent tests green; both unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,9 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:
|
||||
import { runCliInstaller } from "./cli-installer"
|
||||
import * as configManager from "./config-manager"
|
||||
import * as codexInstaller from "./install-codex"
|
||||
import * as claudeCodeInstaller from "./install-claudecode"
|
||||
import type { CodexInstallResult } from "./install-codex"
|
||||
import type { ClaudeCodeInstallResult } from "./install-claudecode"
|
||||
import type { InstallArgs } from "./types"
|
||||
|
||||
const codexResult: CodexInstallResult = {
|
||||
@@ -14,6 +16,11 @@ const codexResult: CodexInstallResult = {
|
||||
codexHome: "/tmp/codex-home",
|
||||
}
|
||||
|
||||
const claudeCodeResult: ClaudeCodeInstallResult = {
|
||||
marketplaceName: "sisyphuslabs",
|
||||
pluginRef: "omo@sisyphuslabs",
|
||||
}
|
||||
|
||||
function createOpenCodeArgs(platform: "opencode" | "both"): InstallArgs {
|
||||
return {
|
||||
tui: false,
|
||||
@@ -145,4 +152,37 @@ describe("runCliInstaller platform branching", () => {
|
||||
// then
|
||||
expect(result).toBe(0)
|
||||
})
|
||||
|
||||
test("runs only Claude Code installation and skips OpenCode/Codex for platform=claudecode", async () => {
|
||||
// given
|
||||
const versionSpy = spyOn(configManager, "getOpenCodeVersion")
|
||||
const writeSpy = spyOn(configManager, "writeOmoConfig")
|
||||
const codexSpy = spyOn(codexInstaller, "runCodexInstaller").mockResolvedValue(codexResult)
|
||||
const claudeCodeSpy = spyOn(claudeCodeInstaller, "runClaudeCodeInstaller").mockResolvedValue(
|
||||
claudeCodeResult,
|
||||
)
|
||||
|
||||
// when
|
||||
const result = await runCliInstaller({ tui: false, platform: "claudecode" }, "3.4.0")
|
||||
|
||||
// then
|
||||
expect(result).toBe(0)
|
||||
expect(versionSpy).not.toHaveBeenCalled()
|
||||
expect(writeSpy).not.toHaveBeenCalled()
|
||||
expect(codexSpy).not.toHaveBeenCalled()
|
||||
expect(claudeCodeSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("fails when Claude-Code-only installation cannot install the plugin", async () => {
|
||||
// given
|
||||
spyOn(claudeCodeInstaller, "runClaudeCodeInstaller").mockRejectedValue(
|
||||
new Error("claude code failed"),
|
||||
)
|
||||
|
||||
// when
|
||||
const result = await runCliInstaller({ tui: false, platform: "claudecode" }, "3.4.0")
|
||||
|
||||
// then
|
||||
expect(result).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "./install-validators"
|
||||
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
||||
import { runCodexInstaller } from "./install-codex"
|
||||
import { runClaudeCodeInstaller } from "./install-claudecode"
|
||||
|
||||
export async function runCliInstaller(args: InstallArgs, version: string): Promise<number> {
|
||||
const validation = validateNonTuiArgs(args)
|
||||
@@ -154,6 +155,19 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
console.log()
|
||||
}
|
||||
|
||||
if (config.hasClaudeCode) {
|
||||
printInfo("Installing Claude Code plugin...")
|
||||
try {
|
||||
const claudeResult = await runClaudeCodeInstaller()
|
||||
printSuccess(`Claude Code plugin installed ${SYMBOLS.arrow} ${color.dim(claudeResult.pluginRef)}`)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
printError(`Claude Code install failed: ${message}`)
|
||||
return 1
|
||||
}
|
||||
console.log()
|
||||
}
|
||||
|
||||
printInfo(
|
||||
"Anonymous telemetry is enabled by default. Disable it with OMO_SEND_ANONYMOUS_TELEMETRY=0 or OMO_DISABLE_POSTHOG=1.",
|
||||
)
|
||||
|
||||
+18
-5
@@ -23,7 +23,7 @@ type InstallCommandOptions = {
|
||||
readonly openai?: InstallArgs["openai"]
|
||||
readonly gemini?: InstallArgs["gemini"]
|
||||
readonly copilot?: InstallArgs["copilot"]
|
||||
readonly platform?: InstallArgs["platform"]
|
||||
readonly platform?: InstallPlatformOption
|
||||
readonly opencodeZen?: InstallArgs["opencodeZen"]
|
||||
readonly zaiCodingPlan?: InstallArgs["zaiCodingPlan"]
|
||||
readonly kimiForCoding?: InstallArgs["kimiForCoding"]
|
||||
@@ -33,21 +33,34 @@ type InstallCommandOptions = {
|
||||
}
|
||||
|
||||
type Environment = Readonly<Record<string, string | undefined>>
|
||||
type InstallPlatformOption = InstallArgs["platform"] | "cc"
|
||||
|
||||
function normalizePlatform(platform: InstallPlatformOption | undefined): InstallArgs["platform"] {
|
||||
return platform === "cc" ? "claudecode" : platform
|
||||
}
|
||||
|
||||
function platformForInvocation(
|
||||
invocationName: string | undefined,
|
||||
env: Environment,
|
||||
): InstallArgs["platform"] {
|
||||
if (invocationName === "lazycodex" && isLazycodexPublishingEnabled(env)) return "codex"
|
||||
if (invocationName === "lazyclaudecode") return "claudecode"
|
||||
return 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 platform = normalizePlatform(options.platform)
|
||||
return {
|
||||
tui: options.tui !== false,
|
||||
claude: options.claude,
|
||||
openai: options.openai,
|
||||
gemini: options.gemini,
|
||||
copilot: options.copilot,
|
||||
platform: options.platform ?? defaultPlatform,
|
||||
platform: platform ?? platformForInvocation(invocationName, env),
|
||||
opencodeZen: options.opencodeZen,
|
||||
zaiCodingPlan: options.zaiCodingPlan,
|
||||
kimiForCoding: options.kimiForCoding,
|
||||
@@ -73,7 +86,7 @@ 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"]))
|
||||
.addOption(new Option("--platform <platform>", "Install target platform: opencode, codex, claudecode (alias: cc), both").choices(["opencode", "codex", "claudecode", "cc", "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)")
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { spawn } from "../../shared/bun-spawn-shim"
|
||||
import type { RunCommand } from "./types"
|
||||
|
||||
export const defaultRunCommand: RunCommand = async (command, args, options) => {
|
||||
const proc = spawn({
|
||||
cmd: [command, ...args],
|
||||
env: options.env as NodeJS.ProcessEnv | undefined,
|
||||
stdin: "ignore",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
|
||||
const code = await proc.exited
|
||||
if (code !== 0) {
|
||||
throw new Error(`${command} ${args.join(" ")} failed with exit code ${code}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type { ClaudeCodeInstallOptions, ClaudeCodeInstallResult, RunCommand } from "./types"
|
||||
export { runClaudeCodeInstaller } from "./install-claudecode"
|
||||
export { defaultRunCommand } from "./claudecode-process"
|
||||
@@ -0,0 +1,95 @@
|
||||
/// <reference path="../../../bun-test.d.ts" />
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { existsSync } from "node:fs"
|
||||
import { mkdtemp, readdir } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { runClaudeCodeInstaller } from "./install-claudecode"
|
||||
|
||||
describe("runClaudeCodeInstaller", () => {
|
||||
test("shells out to claude plugin marketplace add then plugin install", async () => {
|
||||
// given
|
||||
const calls: { command: string; args: readonly string[] }[] = []
|
||||
const runCommand = async (command: string, args: readonly string[]) => {
|
||||
calls.push({ command, args })
|
||||
}
|
||||
|
||||
// when
|
||||
const result = await runClaudeCodeInstaller({ runCommand })
|
||||
|
||||
// then
|
||||
expect(calls.length).toBe(2)
|
||||
expect(calls[0]?.command).toBe("claude")
|
||||
expect(calls[0]?.args).toEqual(["plugin", "marketplace", "add", "code-yeongyu/lazyclaudecode"])
|
||||
expect(calls[1]?.command).toBe("claude")
|
||||
expect(calls[1]?.args).toEqual(["plugin", "install", "omo@sisyphuslabs"])
|
||||
expect(result.marketplaceName).toBe("sisyphuslabs")
|
||||
expect(result.pluginRef).toBe("omo@sisyphuslabs")
|
||||
})
|
||||
|
||||
test("prints the two /plugin commands and throws when claude is absent, writing nothing to ~/.claude", async () => {
|
||||
// given: an isolated HOME so we can assert ~/.claude is never written
|
||||
const fakeHome = await mkdtemp(join(tmpdir(), "omo-cc-home-"))
|
||||
// a PATH that contains no `claude` binary
|
||||
const emptyBinDir = await mkdtemp(join(tmpdir(), "omo-cc-emptybin-"))
|
||||
const printed: string[] = []
|
||||
const claudeHome = join(fakeHome, ".claude")
|
||||
|
||||
// when / then
|
||||
let thrown: unknown
|
||||
try {
|
||||
await runClaudeCodeInstaller({
|
||||
homeDir: fakeHome,
|
||||
env: { PATH: emptyBinDir, HOME: fakeHome },
|
||||
log: (message) => printed.push(message),
|
||||
})
|
||||
} catch (error) {
|
||||
thrown = error
|
||||
}
|
||||
|
||||
// then: it threw a clear error
|
||||
expect(thrown).toBeInstanceOf(Error)
|
||||
expect((thrown as Error).message.toLowerCase()).toContain("claude")
|
||||
|
||||
// then: it printed BOTH /plugin commands for manual recovery
|
||||
const printedText = printed.join("\n")
|
||||
expect(printedText).toContain("/plugin marketplace add code-yeongyu/lazyclaudecode")
|
||||
expect(printedText).toContain("/plugin install omo@sisyphuslabs")
|
||||
|
||||
// then: NOTHING was written to ~/.claude
|
||||
expect(existsSync(claudeHome)).toBe(false)
|
||||
const entries = await readdir(fakeHome)
|
||||
expect(entries).toEqual([])
|
||||
})
|
||||
|
||||
test("prints the two /plugin commands and throws when claude exits non-zero, writing nothing to ~/.claude", async () => {
|
||||
// given
|
||||
const fakeHome = await mkdtemp(join(tmpdir(), "omo-cc-home-"))
|
||||
const printed: string[] = []
|
||||
const claudeHome = join(fakeHome, ".claude")
|
||||
const runCommand = async () => {
|
||||
throw new Error("claude plugin marketplace add code-yeongyu/lazyclaudecode failed with exit code 1")
|
||||
}
|
||||
|
||||
// when / then
|
||||
let thrown: unknown
|
||||
try {
|
||||
await runClaudeCodeInstaller({
|
||||
homeDir: fakeHome,
|
||||
runCommand,
|
||||
log: (message) => printed.push(message),
|
||||
})
|
||||
} catch (error) {
|
||||
thrown = error
|
||||
}
|
||||
|
||||
// then
|
||||
expect(thrown).toBeInstanceOf(Error)
|
||||
const printedText = printed.join("\n")
|
||||
expect(printedText).toContain("/plugin marketplace add code-yeongyu/lazyclaudecode")
|
||||
expect(printedText).toContain("/plugin install omo@sisyphuslabs")
|
||||
expect(existsSync(claudeHome)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import { homedir } from "node:os"
|
||||
import { defaultRunCommand } from "./claudecode-process"
|
||||
import type { ClaudeCodeInstallOptions, ClaudeCodeInstallResult } from "./types"
|
||||
|
||||
const MARKETPLACE_NAME = "sisyphuslabs"
|
||||
const DEFAULT_MARKETPLACE_REPO = "code-yeongyu/lazyclaudecode"
|
||||
const DEFAULT_PLUGIN_REF = "omo@sisyphuslabs"
|
||||
|
||||
export async function runClaudeCodeInstaller(
|
||||
options: ClaudeCodeInstallOptions = {},
|
||||
): Promise<ClaudeCodeInstallResult> {
|
||||
const marketplaceRepo = options.marketplaceRepo ?? DEFAULT_MARKETPLACE_REPO
|
||||
const pluginRef = options.pluginRef ?? DEFAULT_PLUGIN_REF
|
||||
const runCommand = options.runCommand ?? defaultRunCommand
|
||||
const log = options.log ?? ((message: string) => console.log(message))
|
||||
// homedir is resolved (never written to) so the caller can prove ~/.claude stays untouched.
|
||||
void (options.homeDir ?? homedir())
|
||||
const env = options.env ?? process.env
|
||||
|
||||
const marketplaceAddArgs = ["plugin", "marketplace", "add", marketplaceRepo] as const
|
||||
const pluginInstallArgs = ["plugin", "install", pluginRef] as const
|
||||
|
||||
try {
|
||||
await runCommand("claude", marketplaceAddArgs, { env })
|
||||
await runCommand("claude", pluginInstallArgs, { env })
|
||||
} catch (error) {
|
||||
printManualInstructions(log, marketplaceRepo, pluginRef)
|
||||
const reason = error instanceof Error ? error.message : String(error)
|
||||
throw new Error(
|
||||
`Failed to install the Claude Code plugin via the 'claude' CLI (${reason}). ` +
|
||||
"Install Claude Code first (https://claude.com/claude-code), then run the two /plugin commands printed above.",
|
||||
)
|
||||
}
|
||||
|
||||
await trackClaudeCodeInstallTelemetry()
|
||||
|
||||
return {
|
||||
marketplaceName: MARKETPLACE_NAME,
|
||||
pluginRef,
|
||||
}
|
||||
}
|
||||
|
||||
function printManualInstructions(
|
||||
log: (message: string) => void,
|
||||
marketplaceRepo: string,
|
||||
pluginRef: string,
|
||||
): void {
|
||||
log("Could not drive the 'claude' CLI. Run these inside Claude Code to install manually:")
|
||||
log(` /plugin marketplace add ${marketplaceRepo}`)
|
||||
log(` /plugin install ${pluginRef}`)
|
||||
}
|
||||
|
||||
async function trackClaudeCodeInstallTelemetry(): Promise<void> {
|
||||
try {
|
||||
const { createInstallPostHog, getPostHogDistinctId } = await import("@oh-my-opencode/omo-claude/telemetry")
|
||||
const posthog = createInstallPostHog()
|
||||
posthog.trackActive(getPostHogDistinctId(), "install_completed")
|
||||
await posthog.shutdown()
|
||||
} catch {
|
||||
// no-excuse-ok: catch
|
||||
// telemetry must never break installs
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import process from "node:process"
|
||||
import { resolveInstallArgs } from "../cli-program"
|
||||
import { argsToConfig } from "../install-validators"
|
||||
|
||||
describe("lazyclaudecode install routing", () => {
|
||||
const originalInvocationName = process.env.OMO_INVOCATION_NAME
|
||||
|
||||
afterEach(() => {
|
||||
if (originalInvocationName === undefined) {
|
||||
delete process.env.OMO_INVOCATION_NAME
|
||||
return
|
||||
}
|
||||
process.env.OMO_INVOCATION_NAME = originalInvocationName
|
||||
})
|
||||
|
||||
test("defaults platform to claudecode when invoked as lazyclaudecode without --platform", () => {
|
||||
// given
|
||||
process.env.OMO_INVOCATION_NAME = "lazyclaudecode"
|
||||
|
||||
// when
|
||||
const args = resolveInstallArgs({ tui: false })
|
||||
const config = argsToConfig(args)
|
||||
|
||||
// then
|
||||
expect(args.platform).toBe("claudecode")
|
||||
expect(config.hasClaudeCode).toBe(true)
|
||||
expect(config.hasOpenCode).toBe(false)
|
||||
expect(config.hasCodex).toBe(false)
|
||||
})
|
||||
|
||||
test("respects explicit --platform=opencode when invoked as lazyclaudecode", () => {
|
||||
// given
|
||||
process.env.OMO_INVOCATION_NAME = "lazyclaudecode"
|
||||
|
||||
// when
|
||||
const args = resolveInstallArgs({
|
||||
tui: false,
|
||||
claude: "no",
|
||||
gemini: "no",
|
||||
copilot: "no",
|
||||
platform: "opencode",
|
||||
})
|
||||
const config = argsToConfig(args)
|
||||
|
||||
// then
|
||||
expect(args.platform).toBe("opencode")
|
||||
expect(config.hasClaudeCode).toBe(false)
|
||||
expect(config.hasOpenCode).toBe(true)
|
||||
})
|
||||
|
||||
test("does not resolve lazyclaudecode to codex or opencode by default", () => {
|
||||
// given
|
||||
process.env.OMO_INVOCATION_NAME = "lazyclaudecode"
|
||||
|
||||
// when
|
||||
const args = resolveInstallArgs({ tui: false })
|
||||
|
||||
// then
|
||||
expect(args.platform).not.toBe("codex")
|
||||
expect(args.platform).not.toBe("opencode")
|
||||
expect(args.platform).toBe("claudecode")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface CommandRunOptions {
|
||||
readonly env?: { readonly [key: string]: string | undefined }
|
||||
}
|
||||
|
||||
export type RunCommand = (
|
||||
command: string,
|
||||
args: readonly string[],
|
||||
options: CommandRunOptions,
|
||||
) => Promise<void>
|
||||
|
||||
export interface ClaudeCodeInstallOptions {
|
||||
readonly marketplaceRepo?: string
|
||||
readonly pluginRef?: string
|
||||
readonly runCommand?: RunCommand
|
||||
readonly log?: (message: string) => void
|
||||
readonly homeDir?: string
|
||||
readonly env?: { readonly [key: string]: string | undefined }
|
||||
}
|
||||
|
||||
export interface ClaudeCodeInstallResult {
|
||||
readonly marketplaceName: string
|
||||
readonly pluginRef: string
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { resolveInstallArgs } from "./cli-program"
|
||||
import type { InstallPlatform } from "./types"
|
||||
|
||||
describe("install platform resolution", () => {
|
||||
test("leaves omo install without --platform unresolved for config defaults", () => {
|
||||
@@ -92,6 +93,53 @@ describe("install platform resolution", () => {
|
||||
expect(args.platform).toBe("opencode")
|
||||
})
|
||||
|
||||
test("resolves explicit --platform=claudecode", () => {
|
||||
// given
|
||||
const invocationName = "omo"
|
||||
|
||||
// when
|
||||
const args = resolveInstallArgs({ tui: true, platform: "claudecode" }, invocationName)
|
||||
|
||||
// then
|
||||
expect(args.platform).toBe("claudecode")
|
||||
})
|
||||
|
||||
test("normalizes --platform=cc to claudecode", () => {
|
||||
// given
|
||||
const invocationName = "omo"
|
||||
|
||||
// when
|
||||
const args = resolveInstallArgs(
|
||||
{ tui: true, platform: "cc" as InstallPlatform },
|
||||
invocationName,
|
||||
)
|
||||
|
||||
// then
|
||||
expect(args.platform).toBe("claudecode")
|
||||
})
|
||||
|
||||
test("defaults lazyclaudecode install to claudecode platform", () => {
|
||||
// given
|
||||
const invocationName = "lazyclaudecode"
|
||||
|
||||
// when
|
||||
const args = resolveInstallArgs({ tui: true }, invocationName)
|
||||
|
||||
// then
|
||||
expect(args.platform).toBe("claudecode")
|
||||
})
|
||||
|
||||
test("lets lazyclaudecode install explicitly override to opencode", () => {
|
||||
// given
|
||||
const invocationName = "lazyclaudecode"
|
||||
|
||||
// when
|
||||
const args = resolveInstallArgs({ tui: true, platform: "opencode" }, invocationName)
|
||||
|
||||
// then
|
||||
expect(args.platform).toBe("opencode")
|
||||
})
|
||||
|
||||
test("defines Commander choices so invalid --platform values are rejected", async () => {
|
||||
// given
|
||||
const cliProgramSource = await Bun.file(new URL("./cli-program.ts", import.meta.url)).text()
|
||||
@@ -102,6 +150,6 @@ describe("install platform resolution", () => {
|
||||
// then
|
||||
expect(installBlock).not.toBeNull()
|
||||
expect(installBlock?.[1]).toContain('new Option("--platform <platform>"')
|
||||
expect(installBlock?.[1]).toContain('.choices(["opencode", "codex", "both"])')
|
||||
expect(installBlock?.[1]).toContain('.choices(["opencode", "codex", "claudecode", "cc", "both"])')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -130,6 +130,7 @@ export function validateNonTuiArgs(
|
||||
const platform = resolvePlatform(args)
|
||||
const hasOpenCode = platform === "opencode" || platform === "both"
|
||||
const hasCodexOnly = platform === "codex"
|
||||
const hasClaudeCodeOnly = platform === "claudecode"
|
||||
|
||||
if (platformRequiresLazycodex(platform) && !isLazycodexPublishingEnabled(env)) {
|
||||
errors.push(LAZYCODEX_DISABLED_MESSAGE)
|
||||
@@ -182,6 +183,11 @@ export function validateNonTuiArgs(
|
||||
errors.push(...opencodeFlagErrors)
|
||||
}
|
||||
|
||||
if (hasClaudeCodeOnly) {
|
||||
const opencodeFlagErrors = collectClaudeCodeOnlyOpenCodeFlagErrors(args)
|
||||
errors.push(...opencodeFlagErrors)
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors }
|
||||
}
|
||||
|
||||
@@ -203,10 +209,25 @@ function collectCodexOnlyOpenCodeFlagErrors(args: InstallArgs): string[] {
|
||||
return errors
|
||||
}
|
||||
|
||||
function collectClaudeCodeOnlyOpenCodeFlagErrors(args: InstallArgs): string[] {
|
||||
const errors: string[] = []
|
||||
if (args.claude !== undefined) errors.push("--claude cannot be used with --platform=claudecode")
|
||||
if (args.openai !== undefined) errors.push("--openai cannot be used with --platform=claudecode")
|
||||
if (args.gemini !== undefined) errors.push("--gemini cannot be used with --platform=claudecode")
|
||||
if (args.copilot !== undefined) errors.push("--copilot cannot be used with --platform=claudecode")
|
||||
if (args.opencodeZen !== undefined) errors.push("--opencode-zen cannot be used with --platform=claudecode")
|
||||
if (args.zaiCodingPlan !== undefined) errors.push("--zai-coding-plan cannot be used with --platform=claudecode")
|
||||
if (args.kimiForCoding !== undefined) errors.push("--kimi-for-coding cannot be used with --platform=claudecode")
|
||||
if (args.opencodeGo !== undefined) errors.push("--opencode-go cannot be used with --platform=claudecode")
|
||||
if (args.vercelAiGateway !== undefined) errors.push("--vercel-ai-gateway cannot be used with --platform=claudecode")
|
||||
return errors
|
||||
}
|
||||
|
||||
export function argsToConfig(args: InstallArgs): InstallConfig {
|
||||
const platform = resolvePlatform(args)
|
||||
const hasOpenCode = platform === "opencode" || platform === "both"
|
||||
const hasCodex = platform === "codex" || platform === "both"
|
||||
const hasClaudeCode = platform === "claudecode"
|
||||
|
||||
return {
|
||||
platform,
|
||||
@@ -217,6 +238,7 @@ export function argsToConfig(args: InstallArgs): InstallConfig {
|
||||
hasGemini: hasOpenCode && args.gemini === "yes",
|
||||
hasCopilot: hasOpenCode && args.copilot === "yes",
|
||||
hasCodex,
|
||||
hasClaudeCode,
|
||||
hasOpencodeZen: hasOpenCode && args.opencodeZen === "yes",
|
||||
hasZaiCodingPlan: hasOpenCode && args.zaiCodingPlan === "yes",
|
||||
hasKimiForCoding: hasOpenCode && args.kimiForCoding === "yes",
|
||||
|
||||
@@ -34,6 +34,7 @@ export async function promptInstallPlatform(
|
||||
): Promise<InstallPlatform | null> {
|
||||
const options: Option<InstallPlatform>[] = [
|
||||
{ value: "opencode", label: "OpenCode", hint: "Install OpenCode plugin only" },
|
||||
{ value: "claudecode", label: "Claude Code", hint: "Install the omo Claude Code plugin only" },
|
||||
]
|
||||
if (lazycodexEnabled) {
|
||||
options.push(
|
||||
@@ -57,6 +58,7 @@ export async function promptInstallConfig(
|
||||
): Promise<InstallConfig | null> {
|
||||
const hasOpenCode = platform === "opencode" || platform === "both"
|
||||
const hasCodex = platform === "codex" || platform === "both"
|
||||
const hasClaudeCode = platform === "claudecode"
|
||||
|
||||
if (!hasOpenCode) {
|
||||
return {
|
||||
@@ -68,6 +70,7 @@ export async function promptInstallConfig(
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasCodex,
|
||||
hasClaudeCode,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
@@ -178,6 +181,7 @@ export async function promptInstallConfig(
|
||||
hasGemini: gemini === "yes",
|
||||
hasCopilot: copilot === "yes",
|
||||
hasCodex,
|
||||
hasClaudeCode,
|
||||
hasOpencodeZen: opencodeZen === "yes",
|
||||
hasZaiCodingPlan: zaiCodingPlan === "yes",
|
||||
hasKimiForCoding: kimiForCoding === "yes",
|
||||
|
||||
@@ -13,6 +13,7 @@ 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 { runClaudeCodeInstaller } from "./install-claudecode"
|
||||
import {
|
||||
LAZYCODEX_DISABLED_MESSAGE,
|
||||
isLazycodexPublishingEnabled,
|
||||
@@ -135,6 +136,20 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
|
||||
}
|
||||
}
|
||||
|
||||
if (config.hasClaudeCode) {
|
||||
spinner.start("Installing Claude Code plugin")
|
||||
try {
|
||||
const claudeResult = await runClaudeCodeInstaller()
|
||||
spinner.stop(`Claude Code plugin installed (${color.cyan(claudeResult.pluginRef)})`)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
spinner.stop(`Claude Code install failed ${color.yellow("[!]")}`)
|
||||
p.log.error(`Claude Code install failed: ${message}`)
|
||||
p.outro(color.red("Installation failed."))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
p.log.success(color.bold(isUpdate ? "Configuration updated!" : "Installation complete!"))
|
||||
if (config.hasOpenCode) {
|
||||
p.log.message(`Run ${color.cyan("opencode")} to start!`)
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
export type ClaudeSubscription = "no" | "yes" | "max20"
|
||||
export type BooleanArg = "no" | "yes"
|
||||
export type InstallPlatform = "opencode" | "codex" | "both"
|
||||
export type InstallPlatform = "opencode" | "codex" | "claudecode" | "both"
|
||||
|
||||
export interface InstallArgs {
|
||||
tui: boolean
|
||||
@@ -26,6 +26,7 @@ export interface InstallConfig {
|
||||
hasGemini: boolean
|
||||
hasCopilot: boolean
|
||||
hasCodex: boolean
|
||||
hasClaudeCode: boolean
|
||||
hasOpencodeZen: boolean
|
||||
hasZaiCodingPlan: boolean
|
||||
hasKimiForCoding: boolean
|
||||
|
||||
Reference in New Issue
Block a user