mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-28 09:23:05 +08:00
fix installer bootstrap for do/omo/dev initialization
This commit is contained in:
107
bin/cli.js
107
bin/cli.js
@@ -15,6 +15,8 @@ const API_HEADERS = {
|
|||||||
"User-Agent": "myclaude-npx",
|
"User-Agent": "myclaude-npx",
|
||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
};
|
};
|
||||||
|
const WRAPPER_REQUIRED_MODULES = new Set(["do", "omo"]);
|
||||||
|
const WRAPPER_REQUIRED_SKILLS = new Set(["dev"]);
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const out = {
|
const out = {
|
||||||
@@ -499,9 +501,19 @@ async function updateInstalledModules(installDir, tag, config, dryRun) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await fs.promises.mkdir(installDir, { recursive: true });
|
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) {
|
for (const name of toUpdate) {
|
||||||
|
if (WRAPPER_REQUIRED_MODULES.has(name)) await ensureWrapperInstalled();
|
||||||
process.stdout.write(`Updating module: ${name}\n`);
|
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);
|
upsertModuleStatus(installDir, r);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -777,7 +789,57 @@ async function rmTree(p) {
|
|||||||
await fs.promises.rmdir(p, { recursive: true });
|
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];
|
const mod = config && config.modules && config.modules[moduleName];
|
||||||
if (!mod) throw new Error(`Unknown module: ${moduleName}`);
|
if (!mod) throw new Error(`Unknown module: ${moduleName}`);
|
||||||
const ops = Array.isArray(mod.operations) ? mod.operations : [];
|
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") {
|
if (cmd !== "bash install.sh") {
|
||||||
throw new Error(`Refusing run_command: ${cmd || "(empty)"}`);
|
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);
|
await runInstallSh(repoRoot, installDir, tag);
|
||||||
|
if (installState) installState.wrapperInstalled = true;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported operation type: ${type}`);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1023,20 +1103,37 @@ async function installSelected(picks, tag, config, installDir, force, dryRun) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await fs.promises.mkdir(installDir, { recursive: true });
|
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) {
|
for (const p of picks) {
|
||||||
if (p.kind === "wrapper") {
|
if (p.kind === "wrapper") {
|
||||||
process.stdout.write("Installing codeagent-wrapper...\n");
|
await ensureWrapperInstalled();
|
||||||
await runInstallSh(repoRoot, installDir, tag);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (p.kind === "module") {
|
if (p.kind === "module") {
|
||||||
|
if (WRAPPER_REQUIRED_MODULES.has(p.moduleName)) await ensureWrapperInstalled();
|
||||||
process.stdout.write(`Installing module: ${p.moduleName}\n`);
|
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);
|
upsertModuleStatus(installDir, r);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (p.kind === "skill") {
|
if (p.kind === "skill") {
|
||||||
|
if (WRAPPER_REQUIRED_SKILLS.has(p.skillName)) await ensureWrapperInstalled();
|
||||||
process.stdout.write(`Installing skill: ${p.skillName}\n`);
|
process.stdout.write(`Installing skill: ${p.skillName}\n`);
|
||||||
await copyDirRecursive(
|
await copyDirRecursive(
|
||||||
path.join(repoRoot, "skills", p.skillName),
|
path.join(repoRoot, "skills", p.skillName),
|
||||||
|
|||||||
44
install.py
44
install.py
@@ -24,6 +24,7 @@ except ImportError: # pragma: no cover
|
|||||||
|
|
||||||
DEFAULT_INSTALL_DIR = "~/.claude"
|
DEFAULT_INSTALL_DIR = "~/.claude"
|
||||||
SETTINGS_FILE = "settings.json"
|
SETTINGS_FILE = "settings.json"
|
||||||
|
WRAPPER_REQUIRED_MODULES = {"do", "omo"}
|
||||||
|
|
||||||
|
|
||||||
def _ensure_list(ctx: Dict[str, Any], key: str) -> List[Any]:
|
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(),
|
"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", []):
|
for op in cfg.get("operations", []):
|
||||||
op_type = op.get("type")
|
op_type = op.get("type")
|
||||||
try:
|
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():
|
for key, value in op.get("env", {}).items():
|
||||||
env[key] = value.replace("${install_dir}", str(ctx["install_dir"]))
|
env[key] = value.replace("${install_dir}", str(ctx["install_dir"]))
|
||||||
|
|
||||||
command = op.get("command", "")
|
raw_command = str(op.get("command", "")).strip()
|
||||||
if sys.platform == "win32" and command.strip() == "bash install.sh":
|
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"
|
command = "cmd /c install.bat"
|
||||||
|
|
||||||
# Stream output in real-time while capturing for logging
|
# 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:
|
if process.returncode != 0:
|
||||||
raise RuntimeError(f"Command failed with code {process.returncode}: {command}")
|
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:
|
def write_log(entry: Dict[str, Any], ctx: Dict[str, Any]) -> None:
|
||||||
log_path = Path(ctx["log_file"])
|
log_path = Path(ctx["log_file"])
|
||||||
|
|||||||
Reference in New Issue
Block a user