fix(mimo): keep MiMo's login and sessions by injecting the status plugin via MIMOCODE_CONFIG_DIR (#33668)

Co-authored-by: BluesYoung-web <55608642+BluesYoung-web@users.noreply.github.com>
This commit is contained in:
Neil
2026-10-25 19:22:12 -07:00
committed by GitHub
co-authored by BluesYoung-web
parent 1ff73e808d
commit bb47718fbe
26 changed files with 314 additions and 148 deletions
@@ -42,7 +42,7 @@ if [[ -n "${ORCA_DATA_ACCOUNT_DATA_HOME:-}" ]]; then
export OPENCODE_DB="opencode.db"
fi
fi
[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"
[[ -n "${ORCA_MIMOCODE_CONFIG_DIR:-}" ]] && export MIMOCODE_CONFIG_DIR="${ORCA_MIMOCODE_CONFIG_DIR}"
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
@@ -118,7 +118,7 @@ __orca_deferred_init() {
export OPENCODE_DB="opencode.db"
fi
fi
[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"
[[ -n "${ORCA_MIMOCODE_CONFIG_DIR:-}" ]] && export MIMOCODE_CONFIG_DIR="${ORCA_MIMOCODE_CONFIG_DIR}"
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
@@ -50,7 +50,7 @@ if [[ -n "${ORCA_DATA_ACCOUNT_DATA_HOME:-}" ]]; then
export OPENCODE_DB="opencode.db"
fi
fi
[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"
[[ -n "${ORCA_MIMOCODE_CONFIG_DIR:-}" ]] && export MIMOCODE_CONFIG_DIR="${ORCA_MIMOCODE_CONFIG_DIR}"
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
@@ -125,7 +125,7 @@ __orca_deferred_init() {
export OPENCODE_DB="opencode.db"
fi
fi
[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"
[[ -n "${ORCA_MIMOCODE_CONFIG_DIR:-}" ]] && export MIMOCODE_CONFIG_DIR="${ORCA_MIMOCODE_CONFIG_DIR}"
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
# their normal argv shape.
@@ -24,7 +24,7 @@ fi
[[ $- == *i* ]] && bind 'set enable-bracketed-paste on' 2>/dev/null
# Why: remote startup files can re-export user defaults after relay spawn.
[[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"
[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"
[[ -n "${ORCA_MIMOCODE_CONFIG_DIR:-}" ]] && export MIMOCODE_CONFIG_DIR="${ORCA_MIMOCODE_CONFIG_DIR}"
[[ -n "${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac
if [ -n "${ORCA_CLI_BIN_DIR:-}" ]; then
case "${PATH:-}" in
@@ -100,7 +100,7 @@ __orca_deferred_init() {
export OPENCODE_DB="opencode.db"
fi
fi
[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"
[[ -n "${ORCA_MIMOCODE_CONFIG_DIR:-}" ]] && export MIMOCODE_CONFIG_DIR="${ORCA_MIMOCODE_CONFIG_DIR}"
[[ -n "${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac
# Why: OMP does not auto-load Orca's managed status extension; wrap only
# interactive launch invocations so subcommands such as `omp config` keep
@@ -37,7 +37,7 @@ ${ORCA_CLI_POSIX_PATH_RESTORE}
# Restore the managed OpenCode config after user profiles may have replaced it.
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
${MANAGED_DATA_ACCOUNT_POSIX_RESTORE}
[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
[[ -n "\${ORCA_MIMOCODE_CONFIG_DIR:-}" ]] && export MIMOCODE_CONFIG_DIR="\${ORCA_MIMOCODE_CONFIG_DIR}"
${getPosixOmpShellWrapper()}
# Preserve the managed CODEX_HOME after user profiles.
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
@@ -151,8 +151,8 @@ describe('createPtySubprocess', () => {
rows: 24,
env: {
SHELL: '/bin/zsh',
MIMOCODE_HOME: '/tmp/orca-mimocode-overlay',
ORCA_MIMOCODE_HOME: '/tmp/orca-mimocode-overlay'
MIMOCODE_CONFIG_DIR: '/tmp/orca-mimocode-overlay',
ORCA_MIMOCODE_CONFIG_DIR: '/tmp/orca-mimocode-overlay'
}
})
} finally {
@@ -7,7 +7,7 @@ import { join } from 'node:path'
const ORCA_SHELL_WRAPPER_ENV = [
'ORCA_OPENCODE_CONFIG_DIR',
'ORCA_MIMOCODE_HOME',
'ORCA_MIMOCODE_CONFIG_DIR',
'ORCA_PI_CODING_AGENT_DIR',
'ORCA_OMP_CODING_AGENT_DIR',
'ORCA_OMP_STATUS_EXTENSION',
+1 -1
View File
@@ -552,7 +552,7 @@ describePosix('daemon shell-ready launch config', () => {
const restoreLine =
'[[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"'
const mimoRestoreLine =
'[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"'
'[[ -n "${ORCA_MIMOCODE_CONFIG_DIR:-}" ]] && export MIMOCODE_CONFIG_DIR="${ORCA_MIMOCODE_CONFIG_DIR}"'
const codexRestoreLine =
'[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"'
const agentTeamsPathRestoreLine = '[[ -n "${ORCA_AGENT_TEAMS_SHIM_DIR:-}" ]] || return 0'
+2 -1
View File
@@ -123,9 +123,10 @@ export const openCodeHookServiceModuleMock = () => ({
})
export const mimoHookServiceModuleMock = (): {
mimoCodeHookService: { buildPtyEnv: Mock }
mimoCodeHookService: { carryOverLegacyAuth: Mock; buildPtyEnv: Mock }
} => ({
mimoCodeHookService: {
carryOverLegacyAuth: vi.fn(),
buildPtyEnv: mimoCodeBuildPtyEnvMock
}
})
+4 -2
View File
@@ -221,8 +221,10 @@ export function createPtyIpcSuiteEnvironment(): PtyIpcSuiteEnvironment {
? '/tmp/orca-opencode2-overlay'
: '/tmp/orca-opencode2-config'
}))
mimoCodeBuildPtyEnvMock.mockImplementation((_ptyId: string, existingHome?: string) => ({
MIMOCODE_HOME: existingHome ? '/tmp/orca-mimocode-overlay' : '/tmp/orca-mimocode-shared'
mimoCodeBuildPtyEnvMock.mockImplementation((_ptyId: string, existingConfigDir?: string) => ({
MIMOCODE_CONFIG_DIR: existingConfigDir
? '/tmp/orca-mimocode-overlay'
: '/tmp/orca-mimocode-shared'
}))
buildAgentHookEnvMock.mockReturnValue({
ORCA_AGENT_HOOK_PORT: '5678',
+22 -18
View File
@@ -26,7 +26,7 @@ import {
clearPiAgentShadowEnv,
exposePiManagedExtensionEnv,
isMimoLaunchCommand,
resolveMimocodeSourceHome,
resolveMimocodeSourceConfigDir,
resolvePiAgentSourceDir,
resolveScopedPiAgentSourceDir
} from './pi-agent'
@@ -100,25 +100,29 @@ export function buildPtyHostEnv(
opts,
launchCommandHint
)
if (opts.agentStatusHooksEnabled) {
if (isMimoLaunchCommand(launchCommandHint)) {
const preexistingMimocodeHome = resolveMimocodeSourceHome(baseEnv)
Object.assign(baseEnv, mimoCodeHookService.buildPtyEnv(id, preexistingMimocodeHome))
if (baseEnv.MIMOCODE_HOME) {
baseEnv.ORCA_MIMOCODE_HOME = baseEnv.MIMOCODE_HOME
if (preexistingMimocodeHome) {
baseEnv.ORCA_MIMOCODE_SOURCE_HOME = preexistingMimocodeHome
} else {
delete baseEnv.ORCA_MIMOCODE_SOURCE_HOME
}
// Older builds redirected MIMOCODE_HOME; a nested pane must not inherit that overlay.
restoreOrStripOverlayEnv(baseEnv, {
primary: 'MIMOCODE_HOME',
overlay: 'ORCA_MIMOCODE_HOME',
source: 'ORCA_MIMOCODE_SOURCE_HOME'
})
restoreOrStripOverlayEnv(baseEnv, {
primary: 'MIMOCODE_CONFIG_DIR',
overlay: 'ORCA_MIMOCODE_CONFIG_DIR',
source: 'ORCA_MIMOCODE_SOURCE_CONFIG_DIR',
preserveExplicitPrimary: true
})
const mimoSourceConfigDir = resolveMimocodeSourceConfigDir(baseEnv)
if (opts.agentStatusHooksEnabled && isMimoLaunchCommand(launchCommandHint)) {
mimoCodeHookService.carryOverLegacyAuth({ ...process.env, ...baseEnv })
const overlay = mimoCodeHookService.buildPtyEnv(id, mimoSourceConfigDir).MIMOCODE_CONFIG_DIR
if (overlay && overlay !== mimoSourceConfigDir) {
baseEnv.MIMOCODE_CONFIG_DIR = overlay
baseEnv.ORCA_MIMOCODE_CONFIG_DIR = overlay
if (mimoSourceConfigDir) {
baseEnv.ORCA_MIMOCODE_SOURCE_CONFIG_DIR = mimoSourceConfigDir
}
}
} else {
restoreOrStripOverlayEnv(baseEnv, {
primary: 'MIMOCODE_HOME',
overlay: 'ORCA_MIMOCODE_HOME',
source: 'ORCA_MIMOCODE_SOURCE_HOME'
})
}
// Remove inherited hook coordinates before injecting the owning pane's receiver.
+14 -10
View File
@@ -136,7 +136,9 @@ export function getInheritedAgentHookEnvKeysToDelete(
'ORCA_CLAUDE_CONFIG_DIR',
'ORCA_OPENCODE_AGENT',
'ORCA_OPENCODE_CONFIG_DIR',
'ORCA_OPENCODE_SOURCE_CONFIG_DIR'
'ORCA_OPENCODE_SOURCE_CONFIG_DIR',
'ORCA_MIMOCODE_CONFIG_DIR',
'ORCA_MIMOCODE_SOURCE_CONFIG_DIR'
].filter((key) => env[key] === undefined)
}
@@ -155,17 +157,19 @@ export function isMimoLaunchCommand(launchCommand: string | undefined): boolean
return getAgentLaunchExecutableName(launchCommand) === 'mimo'
}
export function resolveMimocodeSourceHome(baseEnv: Record<string, string>): string | undefined {
const sourceHome = baseEnv.ORCA_MIMOCODE_SOURCE_HOME ?? process.env.ORCA_MIMOCODE_SOURCE_HOME
if (sourceHome) {
return sourceHome
/** Call after the overlay env is restored: an explicit pane value wins over inherited markers. */
export function resolveMimocodeSourceConfigDir(
baseEnv: Record<string, string>,
inheritedEnv: NodeJS.ProcessEnv = process.env
): string | undefined {
if (baseEnv.MIMOCODE_CONFIG_DIR) {
return baseEnv.MIMOCODE_CONFIG_DIR
}
const configHome = baseEnv.MIMOCODE_HOME ?? process.env.MIMOCODE_HOME
const orcaHome = baseEnv.ORCA_MIMOCODE_HOME ?? process.env.ORCA_MIMOCODE_HOME
if (configHome && orcaHome && configHome === orcaHome) {
return undefined
if (inheritedEnv.ORCA_MIMOCODE_SOURCE_CONFIG_DIR) {
return inheritedEnv.ORCA_MIMOCODE_SOURCE_CONFIG_DIR
}
return configHome
const configDir = inheritedEnv.MIMOCODE_CONFIG_DIR
return configDir && configDir === inheritedEnv.ORCA_MIMOCODE_CONFIG_DIR ? undefined : configDir
}
export function resolveOpenCodeSourceConfigDir(
@@ -264,9 +264,9 @@ describe('registerPtyHandlers', () => {
const env = await spawnAndGetEnv(undefined, undefined, undefined, undefined, 'mimo')
expect(mimoCodeBuildPtyEnvMock).toHaveBeenCalledTimes(1)
expect(env.MIMOCODE_HOME).toBe('/tmp/orca-mimocode-shared')
expect(env.ORCA_MIMOCODE_HOME).toBe('/tmp/orca-mimocode-shared')
expect(env.ORCA_MIMOCODE_SOURCE_HOME).toBeUndefined()
expect(env.MIMOCODE_CONFIG_DIR).toBe('/tmp/orca-mimocode-shared')
expect(env.ORCA_MIMOCODE_CONFIG_DIR).toBe('/tmp/orca-mimocode-shared')
expect(env.ORCA_MIMOCODE_SOURCE_CONFIG_DIR).toBeUndefined()
})
it.each([
'/usr/local/bin/mimo --prompt hi',
@@ -276,8 +276,8 @@ describe('registerPtyHandlers', () => {
const env = await spawnAndGetEnv(undefined, undefined, undefined, undefined, launchCommand)
expect(mimoCodeBuildPtyEnvMock).toHaveBeenCalledTimes(1)
expect(env.MIMOCODE_HOME).toBe('/tmp/orca-mimocode-shared')
expect(env.ORCA_MIMOCODE_HOME).toBe('/tmp/orca-mimocode-shared')
expect(env.MIMOCODE_CONFIG_DIR).toBe('/tmp/orca-mimocode-shared')
expect(env.ORCA_MIMOCODE_CONFIG_DIR).toBe('/tmp/orca-mimocode-shared')
})
it('uses sequenced startup env as the MiMo launch hint when command is a wrapper', async () => {
const env = await spawnAndGetEnv(
@@ -289,20 +289,20 @@ describe('registerPtyHandlers', () => {
)
expect(mimoCodeBuildPtyEnvMock).toHaveBeenCalledTimes(1)
expect(env.MIMOCODE_HOME).toBe('/tmp/orca-mimocode-shared')
expect(env.ORCA_MIMOCODE_HOME).toBe('/tmp/orca-mimocode-shared')
expect(env.MIMOCODE_CONFIG_DIR).toBe('/tmp/orca-mimocode-shared')
expect(env.ORCA_MIMOCODE_CONFIG_DIR).toBe('/tmp/orca-mimocode-shared')
})
it('does not inject MiMo overlay for non-mimo launches', async () => {
await spawnAndGetEnv()
expect(mimoCodeBuildPtyEnvMock).not.toHaveBeenCalled()
})
it('restores user MiMo home when agent status hooks are disabled in a nested Orca shell', async () => {
it('restores user MiMo config dir when agent status hooks are disabled in a nested Orca shell', async () => {
const env = await spawnAndGetEnv(
{
MIMOCODE_HOME: '/tmp/parent-orca-mimocode-overlay',
ORCA_MIMOCODE_HOME: '/tmp/parent-orca-mimocode-overlay',
ORCA_MIMOCODE_SOURCE_HOME: '/tmp/user-mimocode-home'
MIMOCODE_CONFIG_DIR: '/tmp/parent-orca-mimocode-overlay',
ORCA_MIMOCODE_CONFIG_DIR: '/tmp/parent-orca-mimocode-overlay',
ORCA_MIMOCODE_SOURCE_CONFIG_DIR: '/tmp/user-mimocode-config'
},
undefined,
undefined,
@@ -311,9 +311,27 @@ describe('registerPtyHandlers', () => {
)
expect(mimoCodeBuildPtyEnvMock).not.toHaveBeenCalled()
expect(env.MIMOCODE_CONFIG_DIR).toBe('/tmp/user-mimocode-config')
expect(env.ORCA_MIMOCODE_CONFIG_DIR).toBeUndefined()
expect(env.ORCA_MIMOCODE_SOURCE_CONFIG_DIR).toBeUndefined()
})
it("leaves MiMo's home alone and drops an older build's MIMOCODE_HOME overlay", async () => {
const env = await spawnAndGetEnv(
{
MIMOCODE_HOME: '/tmp/parent-orca-mimocode-home',
ORCA_MIMOCODE_HOME: '/tmp/parent-orca-mimocode-home',
ORCA_MIMOCODE_SOURCE_HOME: '/tmp/user-mimocode-home'
},
undefined,
undefined,
undefined,
'mimo'
)
expect(env.MIMOCODE_HOME).toBe('/tmp/user-mimocode-home')
expect(env.ORCA_MIMOCODE_HOME).toBeUndefined()
expect(env.ORCA_MIMOCODE_SOURCE_HOME).toBeUndefined()
expect(env.MIMOCODE_CONFIG_DIR).toBe('/tmp/orca-mimocode-shared')
})
posixOnlyIt(
'reproduces issue #1534: GUI-launched Orca mirrors zshrc-only OpenCode config',
+104 -38
View File
@@ -1,5 +1,15 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
statSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { setAppEnvironment } from '../../shared/app-environment'
@@ -12,7 +22,7 @@ import { MimoCodeHookService } from './hook-service'
describe('MimoCodeHookService buildPtyEnv', () => {
let userDataDir: string
let mimocodeHome: string
let userConfigDir: string
beforeEach(() => {
userDataDir = mkdtempSync(join(tmpdir(), 'orca-mimocode-userdata-'))
@@ -32,53 +42,109 @@ describe('MimoCodeHookService buildPtyEnv', () => {
getAppMetrics: () => []
})
mimocodeHome = mkdtempSync(join(tmpdir(), 'orca-mimocode-home-'))
const configDir = join(mimocodeHome, 'config')
mkdirSync(join(configDir, 'plugins'), { recursive: true })
writeFileSync(join(configDir, 'mimocode.json'), '{"theme":"dark"}')
writeFileSync(join(configDir, 'plugins', 'user-plugin.js'), 'export default () => {}')
writeFileSync(join(configDir, 'plugins', 'orca-mimocode-status.js'), 'USER PLUGIN')
userConfigDir = mkdtempSync(join(tmpdir(), 'orca-mimocode-config-'))
mkdirSync(join(userConfigDir, 'plugins'), { recursive: true })
writeFileSync(join(userConfigDir, 'mimocode.json'), '{"theme":"dark"}')
writeFileSync(join(userConfigDir, 'plugins', 'user-plugin.js'), 'export default () => {}')
writeFileSync(join(userConfigDir, 'plugins', 'orca-mimocode-status.js'), 'USER PLUGIN')
})
afterEach(() => {
rmSync(userDataDir, { recursive: true, force: true })
rmSync(mimocodeHome, { recursive: true, force: true })
rmSync(userConfigDir, { recursive: true, force: true })
})
it('mirrors user config into shared overlay and installs Orca status plugin', () => {
const service = new MimoCodeHookService()
const env = service.buildPtyEnv('pty-1', mimocodeHome)
// MIMOCODE_HOME also moves auth.json, sessions and state (#9132); only add a config layer.
it('sets only MIMOCODE_CONFIG_DIR to a directory holding just the Orca plugin', () => {
const env = new MimoCodeHookService().buildPtyEnv('pty-1')
const overlayHome = join(userDataDir, 'mimocode-hooks', 'shared')
expect(env.MIMOCODE_HOME).toBe(overlayHome)
expect(readFileSync(join(overlayHome, 'config', 'mimocode.json'), 'utf8')).toBe(
'{"theme":"dark"}'
)
expect(readFileSync(join(overlayHome, 'config', 'plugins', 'user-plugin.js'), 'utf8')).toBe(
'export default () => {}'
)
const orcaPlugin = join(overlayHome, 'config', 'plugins', 'orca-mimocode-status.js')
expect(existsSync(orcaPlugin)).toBe(true)
const pluginSource = readFileSync(orcaPlugin, 'utf8')
const overlay = join(userDataDir, 'mimocode-hooks', 'config')
expect(env).toEqual({ MIMOCODE_CONFIG_DIR: overlay })
expect(readdirSync(overlay)).toEqual(['plugins'])
const pluginSource = readFileSync(join(overlay, 'plugins', 'orca-mimocode-status.js'), 'utf8')
expect(pluginSource).toContain('/hook/mimo-code')
expect(pluginSource).not.toContain('post("SessionStart"')
expect(
readFileSync(join(mimocodeHome, 'config', 'plugins', 'orca-mimocode-status.js'), 'utf8')
).toBe('USER PLUGIN')
})
it('reuses the overlay home on a second buildPtyEnv call', () => {
const service = new MimoCodeHookService()
const first = service.buildPtyEnv('pty-1', mimocodeHome)
const second = service.buildPtyEnv('pty-2', mimocodeHome)
it("layers the user's own MIMOCODE_CONFIG_DIR into the overlay without touching it", () => {
const env = new MimoCodeHookService().buildPtyEnv('pty-1', userConfigDir)
const overlayHome = join(userDataDir, 'mimocode-hooks', 'shared')
expect(first.MIMOCODE_HOME).toBe(overlayHome)
expect(second.MIMOCODE_HOME).toBe(overlayHome)
expect(
readFileSync(join(overlayHome, 'config', 'plugins', 'orca-mimocode-status.js'), 'utf8')
).toContain('/hook/mimo-code')
const overlay = env.MIMOCODE_CONFIG_DIR ?? ''
expect(overlay).toContain(join(userDataDir, 'mimocode-hooks', 'config-overlays'))
expect(readFileSync(join(overlay, 'mimocode.json'), 'utf8')).toBe('{"theme":"dark"}')
expect(readFileSync(join(overlay, 'plugins', 'user-plugin.js'), 'utf8')).toBe(
'export default () => {}'
)
expect(readFileSync(join(overlay, 'plugins', 'orca-mimocode-status.js'), 'utf8')).toContain(
'/hook/mimo-code'
)
expect(readFileSync(join(userConfigDir, 'plugins', 'orca-mimocode-status.js'), 'utf8')).toBe(
'USER PLUGIN'
)
})
it.skipIf(process.platform === 'win32')(
"never writes the Orca plugin through a user's linked plugins folder",
() => {
const linkedPlugins = mkdtempSync(join(tmpdir(), 'orca-mimocode-linked-plugins-'))
writeFileSync(join(linkedPlugins, 'user-plugin.js'), 'export default () => {}')
rmSync(join(userConfigDir, 'plugins'), { recursive: true })
symlinkSync(linkedPlugins, join(userConfigDir, 'plugins'))
try {
const env = new MimoCodeHookService().buildPtyEnv('pty-1', userConfigDir)
expect(existsSync(join(env.MIMOCODE_CONFIG_DIR ?? '', 'plugins', 'user-plugin.js'))).toBe(
true
)
expect(readdirSync(linkedPlugins)).toEqual(['user-plugin.js'])
} finally {
rmSync(linkedPlugins, { recursive: true, force: true })
}
}
)
it('carries a login made under the old Orca MIMOCODE_HOME over once, owner-only', () => {
const legacyData = join(userDataDir, 'mimocode-hooks', 'shared', 'data')
mkdirSync(legacyData, { recursive: true })
writeFileSync(join(legacyData, 'auth.json'), '{"legacy":true}')
const xdgData = join(userDataDir, 'xdg-data')
const service = new MimoCodeHookService()
service.carryOverLegacyAuth({ XDG_DATA_HOME: xdgData })
const auth = join(xdgData, 'mimocode', 'auth.json')
expect(readFileSync(auth, 'utf8')).toBe('{"legacy":true}')
if (process.platform !== 'win32') {
expect(statSync(auth).mode & 0o777).toBe(0o600)
}
expect(existsSync(join(legacyData, 'auth.json'))).toBe(false)
expect(existsSync(join(legacyData, 'auth.json.migrated'))).toBe(true)
rmSync(auth)
service.carryOverLegacyAuth({ XDG_DATA_HOME: xdgData })
expect(existsSync(auth)).toBe(false)
})
it('never overwrites an existing login or follows a relative MIMOCODE_HOME', () => {
const legacyData = join(userDataDir, 'mimocode-hooks', 'shared', 'data')
mkdirSync(legacyData, { recursive: true })
writeFileSync(join(legacyData, 'auth.json'), '{"legacy":true}')
const xdgData = join(userDataDir, 'xdg-data')
mkdirSync(join(xdgData, 'mimocode'), { recursive: true })
const auth = join(xdgData, 'mimocode', 'auth.json')
writeFileSync(auth, '{"canonical":true}')
const service = new MimoCodeHookService()
service.carryOverLegacyAuth({ MIMOCODE_HOME: 'relative-mimo-home' })
expect(existsSync(join(process.cwd(), 'relative-mimo-home'))).toBe(false)
expect(existsSync(join(legacyData, 'auth.json'))).toBe(true)
service.carryOverLegacyAuth({ XDG_DATA_HOME: xdgData })
expect(readFileSync(auth, 'utf8')).toBe('{"canonical":true}')
expect(existsSync(join(legacyData, 'auth.json.migrated'))).toBe(true)
rmSync(auth)
service.carryOverLegacyAuth({ XDG_DATA_HOME: xdgData })
expect(existsSync(auth)).toBe(false)
})
})
+115 -46
View File
@@ -1,81 +1,150 @@
import { getAppEnvironment } from '../../shared/app-environment'
import { join } from 'node:path'
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
import { isAbsolute, join } from 'node:path'
import { homedir } from 'node:os'
import {
chmodSync,
constants,
copyFileSync,
existsSync,
lstatSync,
mkdirSync,
readdirSync,
renameSync,
statSync,
writeFileSync
} from 'node:fs'
import { getOpenCodeFamilyPluginSource } from '../opencode/hook-service'
import { mirrorEntry, safeRemoveTree } from '../pty/overlay-mirror'
import { sourceOverlayDirName } from '../opencode/overlay-dir-names'
import {
clearOpenCodeOverlayManifestEntries,
readOpenCodeOverlayManifest,
writeOpenCodeOverlayManifest,
type OpenCodeOverlayManifest
} from '../opencode/opencode-overlay-manifest'
import { isSafeDescendCandidate, mirrorEntry, safeRemoveOverlay } from '../pty/overlay-mirror'
const ORCA_MIMOCODE_PLUGIN_FILE = 'orca-mimocode-status.js'
const MIMOCODE_HOOKS_DIR = 'mimocode-hooks'
const MIMOCODE_SHARED_HOME = 'shared'
const PLUGIN_ONLY_CONFIG_DIR = 'config'
const SOURCE_CONFIG_OVERLAYS_DIR = 'config-overlays'
const LEGACY_SHARED_HOME = 'shared'
function defaultMimocodeConfigDir(): string {
return join(homedir(), '.config', 'mimocode')
}
function resolveSourceConfigDir(existingHome: string | undefined): string | undefined {
if (existingHome) {
const fromHome = join(existingHome, 'config')
if (existsSync(fromHome)) {
return fromHome
}
function isDirectoryFollowingLinks(path: string): boolean {
try {
return statSync(path).isDirectory()
} catch {
return false
}
const xdg = defaultMimocodeConfigDir()
return existsSync(xdg) ? xdg : undefined
}
function mirrorInto(sourcePath: string, targetDir: string, name: string): void {
// MiMo may have created a same-named file in the overlay since the last launch.
safeRemoveOverlay(join(targetDir, name), targetDir)
mirrorEntry(sourcePath, join(targetDir, name))
}
// Keep MiMo-written overlay files (installed plugin deps); replace only what was mirrored last time.
function mirrorConfigDir(sourceConfigDir: string, targetConfigDir: string): void {
mkdirSync(targetConfigDir, { recursive: true })
clearOpenCodeOverlayManifestEntries(
targetConfigDir,
readOpenCodeOverlayManifest(targetConfigDir),
ORCA_MIMOCODE_PLUGIN_FILE
)
const manifest: OpenCodeOverlayManifest = {
topLevelEntries: [],
pluginEntries: [],
sourceConfigDir
}
for (const entry of readdirSync(sourceConfigDir, { withFileTypes: true })) {
if (entry.name === 'plugins' && entry.isDirectory()) {
const sourcePath = join(sourceConfigDir, entry.name)
// A linked plugins dir must not be linked through, or Orca's plugin lands in the user's folder.
if (entry.name === 'plugins' && isDirectoryFollowingLinks(sourcePath)) {
const overlayPlugins = join(targetConfigDir, 'plugins')
mkdirSync(overlayPlugins, { recursive: true })
for (const pluginEntry of readdirSync(join(sourceConfigDir, 'plugins'), {
withFileTypes: true
})) {
if (pluginEntry.name === ORCA_MIMOCODE_PLUGIN_FILE) {
continue
if (!ensureRealDirectory(overlayPlugins)) {
throw new Error('MiMo overlay plugins folder is not a real directory')
}
for (const pluginName of readdirSync(sourcePath)) {
if (pluginName !== ORCA_MIMOCODE_PLUGIN_FILE) {
mirrorInto(join(sourcePath, pluginName), overlayPlugins, pluginName)
manifest.pluginEntries.push(pluginName)
}
mirrorEntry(
join(sourceConfigDir, 'plugins', pluginEntry.name),
join(overlayPlugins, pluginEntry.name)
)
}
continue
}
mirrorEntry(join(sourceConfigDir, entry.name), join(targetConfigDir, entry.name))
mirrorInto(sourcePath, targetConfigDir, entry.name)
manifest.topLevelEntries.push(entry.name)
}
writeOpenCodeOverlayManifest(targetConfigDir, manifest)
}
function ensureRealDirectory(path: string): boolean {
mkdirSync(path, { recursive: true })
return isSafeDescendCandidate(lstatSync(path))
}
export class MimoCodeHookService {
clearPty(_ptyId: string): void {}
buildPtyEnv(_ptyId: string, existingMimocodeHome?: string): Record<string, string> {
const home = join(
getAppEnvironment().getPath('userData'),
MIMOCODE_HOOKS_DIR,
MIMOCODE_SHARED_HOME
)
/** MIMOCODE_CONFIG_DIR adds a config layer; MIMOCODE_HOME would also move auth and session data. */
buildPtyEnv(_ptyId: string, existingConfigDir?: string): { MIMOCODE_CONFIG_DIR?: string } {
const fallback = { MIMOCODE_CONFIG_DIR: existingConfigDir }
const root = join(getAppEnvironment().getPath('userData'), MIMOCODE_HOOKS_DIR)
const overlay = existingConfigDir
? join(root, SOURCE_CONFIG_OVERLAYS_DIR, sourceOverlayDirName(existingConfigDir))
: join(root, PLUGIN_ONLY_CONFIG_DIR)
try {
for (const sub of ['config', 'data', 'cache', 'state'] as const) {
mkdirSync(join(home, sub), { recursive: true })
if (!ensureRealDirectory(root) || !ensureRealDirectory(overlay)) {
return fallback
}
const overlayConfig = join(home, 'config')
const sourceConfig = resolveSourceConfigDir(existingMimocodeHome)
if (sourceConfig) {
safeRemoveTree(overlayConfig)
mirrorConfigDir(sourceConfig, overlayConfig)
if (existingConfigDir && existsSync(existingConfigDir)) {
mirrorConfigDir(existingConfigDir, overlay)
}
const pluginsDir = join(overlay, 'plugins')
if (!ensureRealDirectory(pluginsDir)) {
return fallback
}
const pluginsDir = join(home, 'config', 'plugins')
mkdirSync(pluginsDir, { recursive: true })
writeFileSync(
join(pluginsDir, ORCA_MIMOCODE_PLUGIN_FILE),
getOpenCodeFamilyPluginSource('/hook/mimo-code', { emitSessionStart: false })
)
} catch {
return existingMimocodeHome ? { MIMOCODE_HOME: existingMimocodeHome } : {}
return fallback
}
return { MIMOCODE_CONFIG_DIR: overlay }
}
/** Older builds kept MiMo data under Orca's MIMOCODE_HOME; move a login made there over once, then mark it migrated. */
carryOverLegacyAuth(env: Record<string, string | undefined>): void {
const legacyAuth = join(
getAppEnvironment().getPath('userData'),
MIMOCODE_HOOKS_DIR,
LEGACY_SHARED_HOME,
'data',
'auth.json'
)
// MiMo rejects a relative MIMOCODE_HOME; Orca would otherwise write relative to its own cwd.
if (env.MIMOCODE_HOME && !isAbsolute(env.MIMOCODE_HOME)) {
return
}
// Mirrors MiMo's Global.Path.data resolution (xdg-basedir, or MIMOCODE_HOME/data).
const dataDir = env.MIMOCODE_HOME
? join(env.MIMOCODE_HOME, 'data')
: join(env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'mimocode')
const auth = join(dataDir, 'auth.json')
try {
if (!existsSync(legacyAuth)) {
return
}
if (!existsSync(auth)) {
mkdirSync(dataDir, { recursive: true })
copyFileSync(legacyAuth, auth, constants.COPYFILE_EXCL)
chmodSync(auth, 0o600)
}
// Never restore the Orca-only login later, e.g. after the user deletes the canonical one.
renameSync(legacyAuth, `${legacyAuth}.migrated`)
} catch {
// A failed carry-over leaves MiMo logged out, as before; never block the launch.
}
return { MIMOCODE_HOME: home }
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ describe('PowerShell OSC 133 bootstrap', () => {
expect(script).toContain('[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()')
expect(script).toContain('ORCA_OPENCODE_CONFIG_DIR')
expect(script).toContain('ORCA_MIMOCODE_HOME')
expect(script).toContain('ORCA_MIMOCODE_CONFIG_DIR')
expect(script).not.toContain('ORCA_PI_CODING_AGENT_DIR')
expect(script).not.toContain('ORCA_OMP_CODING_AGENT_DIR')
expect(script).toContain('ORCA_OMP_STATUS_EXTENSION')
+1 -1
View File
@@ -10,7 +10,7 @@ const POWERSHELL_OSC133_BOOTSTRAP = `# Orca OSC 133 shell integration for PowerS
# Restore managed ownership before the shell-integration compatibility guard.
if ($env:ORCA_OPENCODE_CONFIG_DIR) { $env:OPENCODE_CONFIG_DIR = $env:ORCA_OPENCODE_CONFIG_DIR }
${MANAGED_DATA_ACCOUNT_POWERSHELL_RESTORE}
if ($env:ORCA_MIMOCODE_HOME) { $env:MIMOCODE_HOME = $env:ORCA_MIMOCODE_HOME }
if ($env:ORCA_MIMOCODE_CONFIG_DIR) { $env:MIMOCODE_CONFIG_DIR = $env:ORCA_MIMOCODE_CONFIG_DIR }
if ($env:ORCA_CODEX_HOME) { $env:CODEX_HOME = $env:ORCA_CODEX_HOME }
if ($ExecutionContext.SessionState.LanguageMode -eq "FullLanguage" -and
@@ -522,8 +522,8 @@ describe('LocalPtyProvider', () => {
it('uses shell wrapper when MiMo home must survive shell startup', async () => {
provider.configure({
buildSpawnEnv: (_id, env) => {
env.MIMOCODE_HOME = '/tmp/orca-mimocode-overlay'
env.ORCA_MIMOCODE_HOME = '/tmp/orca-mimocode-overlay'
env.MIMOCODE_CONFIG_DIR = '/tmp/orca-mimocode-overlay'
env.ORCA_MIMOCODE_CONFIG_DIR = '/tmp/orca-mimocode-overlay'
return env
}
})
@@ -43,7 +43,7 @@ ${WSL_MANAGED_CLI_PATH_RESTORE}
# Restore Orca-managed config after user startup files may override it.
[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"
${MANAGED_DATA_ACCOUNT_POSIX_RESTORE}
[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"
[[ -n "\${ORCA_MIMOCODE_CONFIG_DIR:-}" ]] && export MIMOCODE_CONFIG_DIR="\${ORCA_MIMOCODE_CONFIG_DIR}"
${getPosixOmpShellWrapper()}
# Profile scripts must not replace Orca's runtime CODEX_HOME.
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"
@@ -299,7 +299,7 @@ describePosix('local PTY shell-ready launch config', () => {
const restoreLine =
'[[ -n "${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="${ORCA_OPENCODE_CONFIG_DIR}"'
const mimoRestoreLine =
'[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"'
'[[ -n "${ORCA_MIMOCODE_CONFIG_DIR:-}" ]] && export MIMOCODE_CONFIG_DIR="${ORCA_MIMOCODE_CONFIG_DIR}"'
const codexRestoreLine =
'[[ -n "${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="${ORCA_CODEX_HOME}"'
const agentTeamsPathRestoreLine = '[[ -n "${ORCA_AGENT_TEAMS_SHIM_DIR:-}" ]] || return 0'
@@ -125,7 +125,9 @@ describe('resolveWindowsShellLaunchArgs', () => {
const opencodeRestoreIndex = command.indexOf(
'$env:OPENCODE_CONFIG_DIR = $env:ORCA_OPENCODE_CONFIG_DIR'
)
const mimocodeRestoreIndex = command.indexOf('$env:MIMOCODE_HOME = $env:ORCA_MIMOCODE_HOME')
const mimocodeRestoreIndex = command.indexOf(
'$env:MIMOCODE_CONFIG_DIR = $env:ORCA_MIMOCODE_CONFIG_DIR'
)
const duplicateStateGuardIndex = command.indexOf('Test-Path variable:global:__OrcaOsc133State')
const languageModeGuardIndex = command.indexOf('LanguageMode -eq "FullLanguage"')
const ompWrapperIndex = command.indexOf('function Global:omp')
+1 -1
View File
@@ -16,7 +16,7 @@ export type ShellStartupFeature = (typeof SHELL_STARTUP_FEATURES)[number]
const OVERLAY_ENV_KEYS = [
'ORCA_DATA_ACCOUNT_DATA_HOME',
'ORCA_OPENCODE_CONFIG_DIR',
'ORCA_MIMOCODE_HOME',
'ORCA_MIMOCODE_CONFIG_DIR',
'ORCA_OMP_STATUS_EXTENSION',
'ORCA_CODEX_HOME',
'ORCA_AGENT_TEAMS_SHIM_DIR',
@@ -50,7 +50,7 @@ const CONTRACT_GLOBALS = new Set([
// Exported only in the claude function's subshell.
'CLAUDE_CONFIG_DIR',
'HISTFILE',
'MIMOCODE_HOME',
'MIMOCODE_CONFIG_DIR',
'OPENCODE_CONFIG_DIR',
'OPENCODE_AUTH_CONTENT',
'OPENCODE_DB',
+2 -2
View File
@@ -49,7 +49,7 @@ const AGENT_TEAMS_PATH_RESTORE_BLOCK = `__orca_restore_agent_teams_path() {
__orca_restore_agent_teams_path`
const OPENCODE_CONFIG_DIR_RESTORE = `[[ -n "\${ORCA_OPENCODE_CONFIG_DIR:-}" ]] && export OPENCODE_CONFIG_DIR="\${ORCA_OPENCODE_CONFIG_DIR}"`
const MIMOCODE_HOME_RESTORE = `[[ -n "\${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="\${ORCA_MIMOCODE_HOME}"`
const MIMOCODE_CONFIG_DIR_RESTORE = `[[ -n "\${ORCA_MIMOCODE_CONFIG_DIR:-}" ]] && export MIMOCODE_CONFIG_DIR="\${ORCA_MIMOCODE_CONFIG_DIR}"`
const REMOTE_CLI_BIN_DIR_RESTORE = `[[ -n "\${ORCA_REMOTE_CLI_BIN_DIR:-}" ]] && case ":$PATH:" in *:"\${ORCA_REMOTE_CLI_BIN_DIR}":*) ;; *) export PATH="\${ORCA_REMOTE_CLI_BIN_DIR}:$PATH" ;; esac`
const CODEX_HOME_RESTORE = `# Restore the runtime CODEX_HOME after user rc files.
[[ -n "\${ORCA_CODEX_HOME:-}" ]] && export CODEX_HOME="\${ORCA_CODEX_HOME}"`
@@ -93,7 +93,7 @@ function getOverlayRestoreBlocks(spec: ZshStartupHookSpec): (string | null)[] {
spec.restores.agentTeamsPath ? AGENT_TEAMS_PATH_RESTORE_BLOCK : null,
OPENCODE_CONFIG_DIR_RESTORE,
MANAGED_DATA_ACCOUNT_POSIX_RESTORE,
MIMOCODE_HOME_RESTORE,
MIMOCODE_CONFIG_DIR_RESTORE,
spec.restores.remoteCliBinDir ? REMOTE_CLI_BIN_DIR_RESTORE : null,
getPosixOmpShellWrapper(),
spec.restores.codexHome ? CODEX_HOME_RESTORE : null