Files
openclaw/src/commands/codex-runtime-plugin-install.ts
Sarah Fortune 48529f1a96 feat(onboard): offer codex migration after harness install (#81192)
Add a post-install seam so the wizard can prompt the user to import their
existing Codex CLI state (skills, archived config/hooks, advisory cached
plugins) through the existing `openclaw migrate codex` flow once the
harness plugin is in place. Fires on both fresh installs and repair runs;
the user can decline at any time.

Trigger sites, both routing through one helper:

- src/plugins/provider-auth-choice.ts: after
  `ensureCodexRuntimePluginForModelSelection` reports `installed: true`,
  dynamically import `offerPostInstallMigrations` and call it before the
  wizard moves on.
- src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts:
  same call shape with `nonInteractive: true`, so the helper emits a hint
  line only and never mutates state.

Helper (src/wizard/setup.post-install-migration.ts) is generic, not
Codex-hardcoded — it resolves migration providers via the manifest
`migrationProviders` contract, filters to providers owned by plugins the
caller flags as installed in this onboarding step, runs `provider.detect`,
and on TTY hands accepted runs to `migrateDefaultCommand`. All detect,
prompt, and migrate failures are swallowed so onboarding never aborts on
this optional offer.

Also harden the Codex app-server subprocess lifecycle now that `detect()`
runs from a hotter onboarding path: isolate the plugin-install
`plugin/read` call (extensions/codex/src/migration/apply.ts) and have the
isolated request wait for child exit with a SIGKILL fallback
(extensions/codex/src/app-server/request.ts) so parents are not held open
by an orphaned codex binary.

Tests:

- src/wizard/setup.post-install-migration.test.ts (new, 10 cases)
- src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts
  extended with hint-call assertions and a not-required-no-offer case.
2026-05-12 16:51:27 -07:00

127 lines
4.2 KiB
TypeScript

import { existsSync } from "node:fs";
import path from "node:path";
import { modelSelectionShouldEnsureCodexPlugin } from "../agents/openai-codex-routing.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import { enablePluginInConfig } from "../plugins/enable.js";
import { loadInstalledPluginIndexInstallRecords } from "../plugins/installed-plugin-index-records.js";
import type { RuntimeEnv } from "../runtime.js";
import { resolveUserPath } from "../utils.js";
import type { WizardPrompter } from "../wizard/prompts.js";
export const CODEX_RUNTIME_PLUGIN_ID = "codex";
const CODEX_RUNTIME_PLUGIN_LABEL = "Codex";
const CODEX_RUNTIME_PLUGIN_NPM_SPEC = "@openclaw/codex";
function isInstalledRecordPresentOnDisk(
record: PluginInstallRecord | undefined,
env: NodeJS.ProcessEnv,
): boolean {
const installPath = record?.installPath?.trim();
if (!installPath) {
return false;
}
return existsSync(path.join(resolveUserPath(installPath, env), "package.json"));
}
export type CodexRuntimePluginInstallResult = {
cfg: OpenClawConfig;
required: boolean;
installed: boolean;
status?: "installed" | "skipped" | "failed" | "timed_out";
};
export function selectedModelShouldEnsureCodexRuntimePlugin(params: {
cfg: OpenClawConfig;
model?: string;
}): boolean {
return modelSelectionShouldEnsureCodexPlugin({
config: params.cfg,
model: params.model,
});
}
export async function ensureCodexRuntimePluginForModelSelection(params: {
cfg: OpenClawConfig;
model?: string;
prompter: WizardPrompter;
runtime: RuntimeEnv;
workspaceDir?: string;
}): Promise<CodexRuntimePluginInstallResult> {
if (!selectedModelShouldEnsureCodexRuntimePlugin({ cfg: params.cfg, model: params.model })) {
return {
cfg: params.cfg,
required: false,
installed: false,
};
}
const existingRecords = await loadInstalledPluginIndexInstallRecords({ env: process.env });
if (isInstalledRecordPresentOnDisk(existingRecords[CODEX_RUNTIME_PLUGIN_ID], process.env)) {
const repair = await repairCodexRuntimePluginInstallForModelSelection({
cfg: params.cfg,
model: params.model,
env: process.env,
});
for (const change of repair.changes) {
params.runtime.log?.(change);
}
for (const warning of repair.warnings) {
params.runtime.log?.(`Codex update warning: ${warning}`);
}
const enableResult = enablePluginInConfig(params.cfg, CODEX_RUNTIME_PLUGIN_ID);
return {
cfg: enableResult.enabled ? enableResult.config : params.cfg,
required: true,
installed: true,
status: "installed",
};
}
const { ensureOnboardingPluginInstalled } = await import("./onboarding-plugin-install.js");
const result = await ensureOnboardingPluginInstalled({
cfg: params.cfg,
entry: {
pluginId: CODEX_RUNTIME_PLUGIN_ID,
label: CODEX_RUNTIME_PLUGIN_LABEL,
install: {
npmSpec: CODEX_RUNTIME_PLUGIN_NPM_SPEC,
defaultChoice: "npm",
},
trustedSourceLinkedOfficialInstall: true,
preferRemoteInstall: true,
},
prompter: params.prompter,
runtime: params.runtime,
...(params.workspaceDir !== undefined ? { workspaceDir: params.workspaceDir } : {}),
promptInstall: false,
autoConfirmSingleSource: true,
});
return {
cfg: result.cfg,
required: true,
installed: result.installed,
status: result.status,
};
}
export async function repairCodexRuntimePluginInstallForModelSelection(params: {
cfg: OpenClawConfig;
model?: string;
env?: NodeJS.ProcessEnv;
}): Promise<{ required: boolean; changes: string[]; warnings: string[] }> {
if (!selectedModelShouldEnsureCodexRuntimePlugin({ cfg: params.cfg, model: params.model })) {
return { required: false, changes: [], warnings: [] };
}
const { repairMissingPluginInstallsForIds } =
await import("./doctor/shared/missing-configured-plugin-install.js");
const result = await repairMissingPluginInstallsForIds({
cfg: params.cfg,
pluginIds: [CODEX_RUNTIME_PLUGIN_ID],
...(params.env !== undefined ? { env: params.env } : {}),
});
return {
required: true,
changes: result.changes,
warnings: result.warnings,
};
}