Skip to content

Commit 113abf7

Browse files
ralyodioclaude
andcommitted
feat(cli): moshcode upgrade — update moshcode + all installed engines (v0.5.0)
New `upgrade` (alias `update`) subcommand + `/upgrade` in the TUI. With no args it updates everything that has a newer version: moshcode itself (via its install.sh update path) and every *installed* engine. Name targets to narrow it (`upgrade claude`, `upgrade self`, `upgrade engines`). Conductor pattern, no vendoring: each engine is updated with its own native updater when it has one (opencode upgrade, aider --upgrade) and re-run through its idempotent installer otherwise (claude/codex/gemini = npm i -g, which fetches latest). Shared orchestrator in src/upgrade.mjs (planUpgrade + runUpgrade); engines.mjs gains upgradeSpec() + a generic runCmd() spawn helper. Interactive runs hand back to the mosh pit; piped/CI exit with a non-zero code if any upgrade failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8a5228b commit 113abf7

6 files changed

Lines changed: 155 additions & 1 deletion

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ moshcode install claude # npm i -g @anthropic-ai/claude-code
2222
moshcode install codex # npm i -g @openai/codex
2323
```
2424

25+
### Upgrade everything
26+
27+
```sh
28+
moshcode upgrade # update moshcode + every installed engine
29+
moshcode upgrade claude # just one engine (name any; alias ok)
30+
moshcode upgrade self # just moshcode itself
31+
```
32+
33+
Each engine is updated with its own native updater when it has one (e.g.
34+
`opencode upgrade`, `aider --upgrade`) and re-run through its installer
35+
otherwise — moshcode never vendors them. In the TUI: `/upgrade [name…]`.
36+
2537
## PRD — plan before you mosh
2638

2739
Write a product requirements doc *first*, then let your coding agents build to it.

bin/moshcode.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import path from "node:path";
66
import { compile, run } from "../src/interpreter.mjs";
77
import { defaultCommands } from "../src/commands.mjs";
88
import { ENGINES, engineList, engineStatus, resolveEngine, openSession } from "../src/engines.mjs";
9+
import { runUpgrade } from "../src/upgrade.mjs";
910
import { createPrd, listPrds, authoringPrompt } from "../src/prd.mjs";
1011
import { tui } from "../src/tui.mjs";
1112

@@ -55,6 +56,9 @@ usage:
5556
built-in loop if no file); --max bounds
5657
the while loop (default 3)
5758
moshcode install <engine> install an agentic-coding engine
59+
moshcode upgrade [self|<engine>…] update moshcode + all installed engines
60+
(no args = everything; name targets to
61+
narrow, e.g. \`upgrade claude\`)
5862
moshcode prd [idea] publish the next numbered PRD (OpenPRD) to
5963
prd/NNNN-slug.md and hand it to an engine to
6064
author; no arg lists existing PRDs
@@ -105,6 +109,12 @@ async function main() {
105109
});
106110
return;
107111
}
112+
if (cmd === "upgrade" || cmd === "update") {
113+
console.log("🎸 moshcode upgrade — updating moshcode + installed engines 🤘");
114+
const results = await runUpgrade(rest);
115+
const failed = results.filter((r) => !r.ok).length;
116+
return backToPit("upgrade", failed ? 1 : 0);
117+
}
108118
if (cmd === "commands") {
109119
console.log("built-in moshscript commands:\n " + Object.keys(defaultCommands()).map((c) => `${c}()`).join(" "));
110120
return;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "moshcode",
3-
"version": "0.4.0",
3+
"version": "0.5.0",
44
"type": "module",
55
"description": "moshcode — a metal wrapper CLI for agentic coding (installs/drives opencode, claude, codex; spec-driven dev via OpenSpec) + moshscript",
66
"bin": {

src/engines.mjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const ENGINES = {
1111
desc: "opencode — the open-source coding agent (SST/anomalyco)",
1212
bin: "opencode",
1313
install: { cmd: "bash", args: ["-c", "curl -fsSL https://opencode.ai/install | bash"] },
14+
upgrade: { cmd: "opencode", args: ["upgrade"] },
1415
},
1516
claude: {
1617
desc: "Claude Code — Anthropic's agentic CLI",
@@ -42,9 +43,19 @@ export const ENGINES = {
4243
desc: "Aider — pair-programming in your terminal",
4344
bin: "aider",
4445
install: { cmd: "bash", args: ["-c", "curl -LsSf https://aider.chat/install.sh | sh"] },
46+
upgrade: { cmd: "aider", args: ["--upgrade"] },
4547
},
4648
};
4749

50+
/**
51+
* The command that upgrades an already-installed engine in place: its native
52+
* updater if it has one, else re-run the installer (they're idempotent and
53+
* fetch the latest — claude/codex/gemini are `npm i -g` which upgrades).
54+
*/
55+
export function upgradeSpec(engine) {
56+
return engine.upgrade || engine.install;
57+
}
58+
4859
/** Aliases so `/agents cc` etc. resolve. */
4960
const ALIASES = { cc: "claude", "claude-code": "claude", openai: "codex", gpt: "codex", google: "gemini" };
5061

@@ -76,6 +87,21 @@ export function engineList() {
7687
return Object.entries(ENGINES).map(([k, v]) => ` ${k.padEnd(10)} ${v.desc}`).join("\n");
7788
}
7889

90+
/**
91+
* Spawn an arbitrary command with stdio inherited (so its own progress/prompts
92+
* own the terminal). Resolves { ok, code, signal } on exit. Used by install +
93+
* upgrade to run engine installers/updaters.
94+
*/
95+
export function runCmd(cmd, args = []) {
96+
return new Promise((resolve) => {
97+
let child;
98+
try { child = spawn(cmd, args, { stdio: "inherit" }); }
99+
catch (e) { resolve({ ok: false, error: e }); return; }
100+
child.on("error", (e) => resolve({ ok: false, error: e }));
101+
child.on("exit", (code, signal) => resolve({ ok: true, code, signal }));
102+
});
103+
}
104+
79105
/**
80106
* Open a session on an engine: spawn its CLI with stdio inherited so the child
81107
* fully owns the terminal (its own TUI, prompts, colors — full stdin/stdout/

src/tui.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import fs from "node:fs";
88
import os from "node:os";
99
import path from "node:path";
1010
import { ENGINES, resolveEngine, engineStatus, openSession } from "./engines.mjs";
11+
import { runUpgrade } from "./upgrade.mjs";
1112
import { createPrd, listPrds, authoringPrompt } from "./prd.mjs";
1213
import { compile, run } from "./interpreter.mjs";
1314
import { defaultCommands } from "./commands.mjs";
@@ -69,6 +70,7 @@ function printHelp() {
6970
` ${acid("/agents")} list coding engines`,
7071
` ${acid("/agents <name>")} open a session (claude · codex · gemini · aider · opencode)`,
7172
` ${acid("/install <name>")} install an engine`,
73+
` ${acid("/upgrade [name…]")} update moshcode + all installed engines (or just the named ones)`,
7274
` ${acid("/prd [idea]")} publish a numbered PRD (OpenPRD), or list them with no arg`,
7375
` ${acid("/run <file.mosh>")} run a moshscript program`,
7476
` ${acid("/help")} this`,
@@ -78,6 +80,11 @@ function printHelp() {
7880
].join("\n"));
7981
}
8082

83+
async function upgradeAll(targets) {
84+
console.log(info(`upgrading ${bone("moshcode")} + installed engines — hand-off to each tool's updater…`));
85+
await runUpgrade(targets, { log: (s) => console.log(s), rule: () => console.log(hr()) });
86+
}
87+
8188
async function openEngine(key, engine, args) {
8289
if (!engine.installed && !args.length) {
8390
console.log(info(`${key} isn't installed — try ${acid("/install " + key)} first.`));
@@ -169,6 +176,12 @@ export async function tui() {
169176
rl = mkrl();
170177
continue;
171178
}
179+
if (cmd === "upgrade" || cmd === "update") {
180+
rl.close();
181+
await upgradeAll(rest.map((r) => r.toLowerCase()));
182+
rl = mkrl();
183+
continue;
184+
}
172185
if (cmd === "prd") {
173186
if (!rest.length) { printPrds(); continue; }
174187
const idea = rest.join(" ");

src/upgrade.mjs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// `moshcode upgrade` — update everything that has a newer version: moshcode
2+
// itself and every installed coding engine. Conductor pattern: we just re-run
3+
// each tool's own updater/installer (they fetch latest), never vendor them.
4+
import { ENGINES, engineStatus, resolveEngine, upgradeSpec, runCmd } from "./engines.mjs";
5+
6+
// Self-upgrade re-runs the moshcode installer's `update` path. Defaults to the
7+
// GitHub-hosted install.sh (always live); override with MOSHCODE_INSTALL_URL.
8+
const SELF_URL = process.env.MOSHCODE_INSTALL_URL
9+
|| "https://raw.githubusercontent.com/moshcoder/moshcode/main/install.sh";
10+
11+
function selfSpec() {
12+
return { cmd: "sh", args: ["-c", `curl -fsSL ${SELF_URL} | sh -s -- update`] };
13+
}
14+
15+
/**
16+
* Work out an upgrade plan from optional targets:
17+
* []/["all"] → moshcode + every *installed* engine
18+
* ["self"|"moshcode"] → moshcode only
19+
* ["engines"] → all installed engines (no self)
20+
* ["claude", …] → those engines (installs the ones not present yet)
21+
* Returns { self, items:[{key,label,spec,installed}], unknown:[] }.
22+
*/
23+
export function planUpgrade(targets = []) {
24+
const t = targets.map((x) => String(x).trim().toLowerCase()).filter(Boolean);
25+
const status = engineStatus();
26+
const byKey = Object.fromEntries(status.map((e) => [e.key, e]));
27+
28+
const wantsAll = t.length === 0 || t.includes("all");
29+
const wantsSelf = wantsAll || t.includes("self") || t.includes("moshcode");
30+
const wantsEngines = wantsAll || t.includes("engines");
31+
32+
const items = [];
33+
const unknown = [];
34+
const seen = new Set();
35+
36+
const add = (key) => {
37+
if (seen.has(key)) return;
38+
seen.add(key);
39+
const st = byKey[key];
40+
items.push({ key, label: key, spec: upgradeSpec(ENGINES[key]), installed: st.installed });
41+
};
42+
43+
if (wantsEngines) {
44+
for (const e of status) if (e.installed) add(e.key);
45+
}
46+
// Explicitly-named engines/aliases (upgrade even if not among "installed").
47+
for (const tok of t) {
48+
if (["all", "self", "moshcode", "engines"].includes(tok)) continue;
49+
const resolved = resolveEngine(tok);
50+
if (resolved) add(resolved[0]);
51+
else unknown.push(tok);
52+
}
53+
54+
return { self: wantsSelf, items, unknown };
55+
}
56+
57+
/**
58+
* Run an upgrade plan sequentially, streaming each tool's own output. `io.log`
59+
* prints a status line, `io.rule` draws a divider around each hand-off (both
60+
* optional — default to plain console output). Returns a summary array.
61+
*/
62+
export async function runUpgrade(targets = [], io = {}) {
63+
const log = io.log || ((s) => console.log(s));
64+
const rule = io.rule || (() => console.log("─".repeat(48)));
65+
const { self, items, unknown } = planUpgrade(targets);
66+
67+
for (const u of unknown) log(`? skipping unknown engine "${u}"`);
68+
69+
if (!self && items.length === 0) {
70+
if (!unknown.length) log("nothing to upgrade — no engines installed. install one: moshcode install claude");
71+
return [];
72+
}
73+
74+
const results = [];
75+
const run = async (name, spec, note) => {
76+
log(`\n⬆ upgrading ${name}${note ? ` ${note}` : ""}${spec.cmd} ${spec.args.join(" ")}`);
77+
rule();
78+
const r = await runCmd(spec.cmd, spec.args);
79+
rule();
80+
const ok = r.ok && (r.code == null || r.code === 0);
81+
log(ok ? `✓ ${name} up to date` : `✗ ${name} upgrade failed${r.code != null ? ` (code ${r.code})` : r.error ? `: ${r.error.message || r.error}` : ""}`);
82+
results.push({ name, ok, code: r.code });
83+
return ok;
84+
};
85+
86+
if (self) await run("moshcode", selfSpec(), "(self)");
87+
for (const it of items) await run(it.label, it.spec, it.installed ? "" : "(installing — not present)");
88+
89+
const failed = results.filter((r) => !r.ok);
90+
log(`\n${failed.length ? "✗" : "✓"} upgraded ${results.length - failed.length}/${results.length}${failed.length ? ` — failed: ${failed.map((r) => r.name).join(", ")}` : "."} 🤘`);
91+
if (self) log("· restart moshcode to pick up its own new version.");
92+
return results;
93+
}

0 commit comments

Comments
 (0)