fix installer bootstrap for do/omo/dev initialization

This commit is contained in:
cexll
2026-02-22 18:19:29 +08:00
parent 791bd03724
commit 19d411a6a2
2 changed files with 144 additions and 7 deletions

View File

@@ -15,6 +15,8 @@ const API_HEADERS = {
"User-Agent": "myclaude-npx",
Accept: "application/vnd.github+json",
};
const WRAPPER_REQUIRED_MODULES = new Set(["do", "omo"]);
const WRAPPER_REQUIRED_SKILLS = new Set(["dev"]);
function parseArgs(argv) {
const out = {
@@ -499,9 +501,19 @@ async function updateInstalledModules(installDir, tag, config, dryRun) {
}
await fs.promises.mkdir(installDir, { recursive: true });
const installState = { wrapperInstalled: false };
async function ensureWrapperInstalled() {
if (installState.wrapperInstalled) return;
process.stdout.write("Installing codeagent-wrapper...\n");
await runInstallSh(repoRoot, installDir, tag);
installState.wrapperInstalled = true;
}
for (const name of toUpdate) {
if (WRAPPER_REQUIRED_MODULES.has(name)) await ensureWrapperInstalled();
process.stdout.write(`Updating module: ${name}\n`);
const r = await applyModule(name, config, repoRoot, installDir, true, tag);
const r = await applyModule(name, config, repoRoot, installDir, true, tag, installState);
upsertModuleStatus(installDir, r);
}
} finally {
@@ -777,7 +789,57 @@ async function rmTree(p) {
await fs.promises.rmdir(p, { recursive: true });
}
async function applyModule(moduleName, config, repoRoot, installDir, force, tag) {
function defaultModelsConfig() {
return {
default_backend: "codex",
default_model: "gpt-4.1",
backends: {},
agents: {},
};
}
function mergeModuleAgentsToModels(moduleName, mod, repoRoot) {
const moduleAgents = mod && mod.agents;
if (!isPlainObject(moduleAgents) || !Object.keys(moduleAgents).length) return false;
const modelsPath = path.join(os.homedir(), ".codeagent", "models.json");
fs.mkdirSync(path.dirname(modelsPath), { recursive: true });
let models;
if (fs.existsSync(modelsPath)) {
models = JSON.parse(fs.readFileSync(modelsPath, "utf8"));
} else {
const templatePath = path.join(repoRoot, "templates", "models.json.example");
if (fs.existsSync(templatePath)) {
models = JSON.parse(fs.readFileSync(templatePath, "utf8"));
if (!isPlainObject(models)) models = defaultModelsConfig();
models.agents = {};
} else {
models = defaultModelsConfig();
}
}
if (!isPlainObject(models)) models = defaultModelsConfig();
if (!isPlainObject(models.agents)) models.agents = {};
let modified = false;
for (const [agentName, agentCfg] of Object.entries(moduleAgents)) {
if (!isPlainObject(agentCfg)) continue;
const existing = models.agents[agentName];
const canOverwrite = !isPlainObject(existing) || Object.prototype.hasOwnProperty.call(existing, "__module__");
if (!canOverwrite) continue;
const next = { ...agentCfg, __module__: moduleName };
if (!deepEqual(existing, next)) {
models.agents[agentName] = next;
modified = true;
}
}
if (modified) fs.writeFileSync(modelsPath, JSON.stringify(models, null, 2) + "\n", "utf8");
return modified;
}
async function applyModule(moduleName, config, repoRoot, installDir, force, tag, installState) {
const mod = config && config.modules && config.modules[moduleName];
if (!mod) throw new Error(`Unknown module: ${moduleName}`);
const ops = Array.isArray(mod.operations) ? mod.operations : [];
@@ -803,7 +865,12 @@ async function applyModule(moduleName, config, repoRoot, installDir, force, tag)
if (cmd !== "bash install.sh") {
throw new Error(`Refusing run_command: ${cmd || "(empty)"}`);
}
if (installState && installState.wrapperInstalled) {
result.operations.push({ type, status: "success", skipped: true });
continue;
}
await runInstallSh(repoRoot, installDir, tag);
if (installState) installState.wrapperInstalled = true;
} else {
throw new Error(`Unsupported operation type: ${type}`);
}
@@ -834,6 +901,19 @@ async function applyModule(moduleName, config, repoRoot, installDir, force, tag)
});
}
try {
if (mergeModuleAgentsToModels(moduleName, mod, repoRoot)) {
result.has_agents = true;
result.operations.push({ type: "merge_agents", status: "success" });
}
} catch (err) {
result.operations.push({
type: "merge_agents",
status: "failed",
error: err && err.message ? err.message : String(err),
});
}
return result;
}
@@ -1023,20 +1103,37 @@ async function installSelected(picks, tag, config, installDir, force, dryRun) {
}
await fs.promises.mkdir(installDir, { recursive: true });
const installState = { wrapperInstalled: false };
async function ensureWrapperInstalled() {
if (installState.wrapperInstalled) return;
process.stdout.write("Installing codeagent-wrapper...\n");
await runInstallSh(repoRoot, installDir, tag);
installState.wrapperInstalled = true;
}
for (const p of picks) {
if (p.kind === "wrapper") {
process.stdout.write("Installing codeagent-wrapper...\n");
await runInstallSh(repoRoot, installDir, tag);
await ensureWrapperInstalled();
continue;
}
if (p.kind === "module") {
if (WRAPPER_REQUIRED_MODULES.has(p.moduleName)) await ensureWrapperInstalled();
process.stdout.write(`Installing module: ${p.moduleName}\n`);
const r = await applyModule(p.moduleName, config, repoRoot, installDir, force, tag);
const r = await applyModule(
p.moduleName,
config,
repoRoot,
installDir,
force,
tag,
installState
);
upsertModuleStatus(installDir, r);
continue;
}
if (p.kind === "skill") {
if (WRAPPER_REQUIRED_SKILLS.has(p.skillName)) await ensureWrapperInstalled();
process.stdout.write(`Installing skill: ${p.skillName}\n`);
await copyDirRecursive(
path.join(repoRoot, "skills", p.skillName),

View File

@@ -24,6 +24,7 @@ except ImportError: # pragma: no cover
DEFAULT_INSTALL_DIR = "~/.claude"
SETTINGS_FILE = "settings.json"
WRAPPER_REQUIRED_MODULES = {"do", "omo"}
def _ensure_list(ctx: Dict[str, Any], key: str) -> List[Any]:
@@ -898,6 +899,24 @@ def execute_module(name: str, cfg: Dict[str, Any], ctx: Dict[str, Any]) -> Dict[
"installed_at": datetime.now().isoformat(),
}
if name in WRAPPER_REQUIRED_MODULES:
try:
ensure_wrapper_installed(ctx)
result["operations"].append({"type": "ensure_wrapper", "status": "success"})
except Exception as exc: # noqa: BLE001
result["status"] = "failed"
result["operations"].append(
{"type": "ensure_wrapper", "status": "failed", "error": str(exc)}
)
write_log(
{
"level": "ERROR",
"message": f"Module {name} failed on ensure_wrapper: {exc}",
},
ctx,
)
raise
for op in cfg.get("operations", []):
op_type = op.get("type")
try:
@@ -1081,8 +1100,13 @@ def op_run_command(op: Dict[str, Any], ctx: Dict[str, Any]) -> None:
for key, value in op.get("env", {}).items():
env[key] = value.replace("${install_dir}", str(ctx["install_dir"]))
command = op.get("command", "")
if sys.platform == "win32" and command.strip() == "bash install.sh":
raw_command = str(op.get("command", "")).strip()
if raw_command == "bash install.sh" and ctx.get("_wrapper_installed"):
write_log({"level": "INFO", "message": "Skip wrapper install; already installed in this run"}, ctx)
return
command = raw_command
if sys.platform == "win32" and raw_command == "bash install.sh":
command = "cmd /c install.bat"
# Stream output in real-time while capturing for logging
@@ -1156,6 +1180,22 @@ def op_run_command(op: Dict[str, Any], ctx: Dict[str, Any]) -> None:
if process.returncode != 0:
raise RuntimeError(f"Command failed with code {process.returncode}: {command}")
if raw_command == "bash install.sh":
ctx["_wrapper_installed"] = True
def ensure_wrapper_installed(ctx: Dict[str, Any]) -> None:
if ctx.get("_wrapper_installed"):
return
op_run_command(
{
"type": "run_command",
"command": "bash install.sh",
"env": {"INSTALL_DIR": "${install_dir}"},
},
ctx,
)
def write_log(entry: Dict[str, Any], ctx: Dict[str, Any]) -> None:
log_path = Path(ctx["log_file"])