moshcode 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +580 -0
- package/bin/moshcode.mjs +674 -0
- package/bin/moshscript.mjs +29 -0
- package/examples/alive.mosh +6 -0
- package/examples/scripting-the-cli.mosh +21 -0
- package/examples/team-secrets.mosh +20 -0
- package/examples/templates/bun-caddy-sqlite/.env.example +14 -0
- package/examples/templates/bun-caddy-sqlite/Caddyfile +18 -0
- package/examples/templates/bun-caddy-sqlite/README.md +97 -0
- package/examples/templates/bun-caddy-sqlite/deploy/moshcode-dns.service +39 -0
- package/examples/templates/bun-caddy-sqlite/deploy/moshpit-service.service +38 -0
- package/examples/templates/bun-caddy-sqlite/package.json +15 -0
- package/examples/templates/bun-caddy-sqlite/src/db.ts +47 -0
- package/examples/templates/bun-caddy-sqlite/src/server.ts +44 -0
- package/examples/templates/bun-caddy-sqlite/template.json +10 -0
- package/examples/templates/caddy-proxy/Caddyfile +36 -0
- package/examples/templates/caddy-proxy/README.md +104 -0
- package/examples/templates/caddy-proxy/deploy/moshcode-dns.service +39 -0
- package/examples/templates/caddy-proxy/template.json +8 -0
- package/examples/templates/caddy-static/Caddyfile +16 -0
- package/examples/templates/caddy-static/README.md +90 -0
- package/examples/templates/caddy-static/deploy/moshcode-dns.service +39 -0
- package/examples/templates/caddy-static/site/index.html +11 -0
- package/examples/templates/caddy-static/template.json +8 -0
- package/install.sh +194 -0
- package/package.json +28 -0
- package/prd/0000-template.md +49 -0
- package/prd/0001-wrap-ugig-and-coinpay-clis.md +121 -0
- package/prd/0002-separate-agent-and-raw-engine-launches.md +113 -0
- package/prd/0003-cross-engine-mcp-and-skill-installation.md +165 -0
- package/prd/0004-moshscript-run-programmable-moshcode.md +344 -0
- package/prd/0005-hosted-moshpit-resolver.md +192 -0
- package/prd/0006-help.md +359 -0
- package/prd/0007-profullstack-site-init.md +1183 -0
- package/prd/README.md +26 -0
- package/src/ads.mjs +58 -0
- package/src/auth.mjs +193 -0
- package/src/cli-schema.mjs +533 -0
- package/src/cli.mjs +118 -0
- package/src/commands.mjs +259 -0
- package/src/completion.mjs +594 -0
- package/src/console.mjs +244 -0
- package/src/dns-system.mjs +404 -0
- package/src/dns.mjs +2872 -0
- package/src/doh-server.mjs +256 -0
- package/src/doh.mjs +218 -0
- package/src/engines.mjs +385 -0
- package/src/escalate.mjs +85 -0
- package/src/help.mjs +443 -0
- package/src/integrations.mjs +265 -0
- package/src/mcp-catalog.mjs +50 -0
- package/src/mcp.mjs +155 -0
- package/src/mirror.mjs +187 -0
- package/src/notify.mjs +86 -0
- package/src/open-url.mjs +34 -0
- package/src/parking-http.mjs +65 -0
- package/src/pins.mjs +190 -0
- package/src/pit-url.mjs +13 -0
- package/src/prd.mjs +341 -0
- package/src/pty.mjs +176 -0
- package/src/pwd.mjs +103 -0
- package/src/registry.mjs +37 -0
- package/src/release-install.mjs +191 -0
- package/src/runtime.mjs +161 -0
- package/src/selfupdate.mjs +215 -0
- package/src/serve.mjs +502 -0
- package/src/skills.mjs +93 -0
- package/src/tabs.mjs +144 -0
- package/src/templates.mjs +456 -0
- package/src/tools.mjs +231 -0
- package/src/trade.mjs +137 -0
- package/src/trust.mjs +712 -0
- package/src/tui.mjs +736 -0
- package/src/ui.mjs +49 -0
- package/src/uninstall.mjs +113 -0
- package/src/upgrade.mjs +217 -0
package/src/ui.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Metal terminal styling — poison acid-lime (#9EF01A) on near-black, the
|
|
2
|
+
// moshcoding palette. Truecolor ANSI with a NO_COLOR opt-out.
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/** moshcode's own version, read from package.json (best-effort). */
|
|
7
|
+
export function moshcodeVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const pkg = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
10
|
+
return JSON.parse(fs.readFileSync(pkg, "utf8")).version || null;
|
|
11
|
+
} catch { return null; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const useColor = process.env.NO_COLOR == null && process.stdout.isTTY === true;
|
|
15
|
+
const rgb = (r, g, b) => (s) => (useColor ? `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m` : String(s));
|
|
16
|
+
const wrap = (o, c) => (s) => (useColor ? `\x1b[${o}m${s}\x1b[${c}m` : String(s));
|
|
17
|
+
|
|
18
|
+
export const acid = rgb(158, 240, 26);
|
|
19
|
+
export const bone = rgb(238, 242, 232);
|
|
20
|
+
export const ash = rgb(139, 147, 138);
|
|
21
|
+
export const danger = rgb(255, 77, 61);
|
|
22
|
+
export const amber = rgb(255, 213, 61);
|
|
23
|
+
export const spotify = rgb(29, 185, 84);
|
|
24
|
+
export const dim = wrap(2, 22);
|
|
25
|
+
|
|
26
|
+
export const ok = (s) => acid("✓ ") + s;
|
|
27
|
+
export const err = (s) => danger("✗ ") + s;
|
|
28
|
+
export const warn = (s) => amber("⚠ ") + s;
|
|
29
|
+
export const info = (s) => ash("· ") + s;
|
|
30
|
+
|
|
31
|
+
export function banner() {
|
|
32
|
+
const version = moshcodeVersion();
|
|
33
|
+
const name = bone("moshcode") + (version ? ash(" v" + version) : "");
|
|
34
|
+
return [
|
|
35
|
+
acid(" ███╗ ███╗ ██████╗ ███████╗██╗ ██╗"),
|
|
36
|
+
acid(" ████╗ ████║██╔═══██╗██╔════╝██║ ██║") + ash(" code hard,"),
|
|
37
|
+
acid(" ██╔████╔██║██║ ██║███████╗███████║") + ash(" mosh harder"),
|
|
38
|
+
acid(" ██║╚██╔╝██║██║ ██║╚════██║██╔══██║"),
|
|
39
|
+
acid(" ██║ ╚═╝ ██║╚██████╔╝███████║██║ ██║") + dim(" ⚡ #moshcoding"),
|
|
40
|
+
acid(" ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝"),
|
|
41
|
+
"",
|
|
42
|
+
" " + name + ash(" · a wall of distortion for your coding agents"),
|
|
43
|
+
" " + acid("https://moshcode.sh"),
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hr() {
|
|
48
|
+
return ash("─".repeat(Math.min(process.stdout.columns || 60, 60)));
|
|
49
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Taking a tool back off the machine.
|
|
2
|
+
//
|
|
3
|
+
// `moshcode install` runs whatever each tool ships as its installer, and those
|
|
4
|
+
// come in two shapes: `npm install -g <pkg>`, and `curl … | sh`. Only the first
|
|
5
|
+
// has an inverse anyone wrote down. The second drops a binary somewhere and
|
|
6
|
+
// leaves no record, so removing it means finding that binary and deleting it —
|
|
7
|
+
// which is a different kind of operation and is treated like one here.
|
|
8
|
+
//
|
|
9
|
+
// The rules that follow from that:
|
|
10
|
+
//
|
|
11
|
+
// - an npm install is undone by npm, which knows what it put where
|
|
12
|
+
// - a script install is undone by removing the binary the tool reports, and
|
|
13
|
+
// only when it sits somewhere a per-user installer would plausibly have put
|
|
14
|
+
// it. `/usr/bin/git` is not something this should ever offer to delete
|
|
15
|
+
// - anything else says so rather than guessing
|
|
16
|
+
//
|
|
17
|
+
// Plans are data so the whole decision is testable without deleting anything,
|
|
18
|
+
// and so `--dry-run` can show exactly what would go.
|
|
19
|
+
|
|
20
|
+
import { dirname } from "node:path";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Where a per-user installer legitimately puts a binary.
|
|
25
|
+
*
|
|
26
|
+
* A deny-list would be the wrong shape here: the question is not "is this
|
|
27
|
+
* dangerous" but "did something we ran plausibly create this", and only an
|
|
28
|
+
* allow-list answers that. A binary in /usr/bin arrived from the system package
|
|
29
|
+
* manager and is not ours to remove.
|
|
30
|
+
*/
|
|
31
|
+
export function safePrefixes(home = homedir()) {
|
|
32
|
+
return [
|
|
33
|
+
`${home}/.local/bin`,
|
|
34
|
+
`${home}/.bun/bin`,
|
|
35
|
+
`${home}/.cargo/bin`,
|
|
36
|
+
`${home}/.deno/bin`,
|
|
37
|
+
`${home}/bin`,
|
|
38
|
+
`${home}/.npm-global/bin`,
|
|
39
|
+
`${home}/.volta/bin`,
|
|
40
|
+
`${home}/go/bin`,
|
|
41
|
+
"/usr/local/bin",
|
|
42
|
+
"/opt/homebrew/bin",
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The npm package an install spec would install, or null if it is not npm. */
|
|
47
|
+
export function npmPackageOf(install) {
|
|
48
|
+
if (!install) return null;
|
|
49
|
+
const npmish = ["npm", "pnpm", "yarn", "bun"].includes(install.cmd);
|
|
50
|
+
if (!npmish) return null;
|
|
51
|
+
const args = install.args || [];
|
|
52
|
+
if (!args.includes("-g") && !args.includes("--global")) return null;
|
|
53
|
+
// The package is the last argument that is not a flag or a subcommand.
|
|
54
|
+
const skip = new Set(["install", "add", "i", "-g", "--global"]);
|
|
55
|
+
const pkg = [...args].reverse().find((a) => !a.startsWith("-") && !skip.has(a));
|
|
56
|
+
return pkg || null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* How to remove `entry`, given where its binary currently is.
|
|
61
|
+
*
|
|
62
|
+
* `binPath` is what `which <bin>` reported, or null when it is not on PATH.
|
|
63
|
+
*/
|
|
64
|
+
export function uninstallPlan(entry, { binPath = null, home = homedir() } = {}) {
|
|
65
|
+
const pkg = npmPackageOf(entry?.install);
|
|
66
|
+
if (pkg) {
|
|
67
|
+
return {
|
|
68
|
+
kind: "npm",
|
|
69
|
+
steps: [{ kind: "run", command: entry.install.cmd, args: ["uninstall", "-g", pkg] }],
|
|
70
|
+
warnings: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!binPath) {
|
|
75
|
+
return {
|
|
76
|
+
kind: "absent",
|
|
77
|
+
steps: [],
|
|
78
|
+
warnings: [`${entry?.bin || "it"} is not on your PATH — nothing to remove`],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const dir = dirname(binPath);
|
|
83
|
+
if (!safePrefixes(home).includes(dir)) {
|
|
84
|
+
// Refused rather than confirmed-with-a-scary-prompt: a binary here came
|
|
85
|
+
// from somewhere else, and the person who put it there knows how to remove
|
|
86
|
+
// it. Deleting it because a prompt was clicked through is worse than not
|
|
87
|
+
// offering.
|
|
88
|
+
return {
|
|
89
|
+
kind: "refused",
|
|
90
|
+
steps: [],
|
|
91
|
+
warnings: [
|
|
92
|
+
`${binPath} is not in a directory moshcode installs into.`,
|
|
93
|
+
"It was put there by something else — a system package manager, or by hand — so removing it is that thing's job.",
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
kind: "binary",
|
|
100
|
+
steps: [{ kind: "remove", path: binPath }],
|
|
101
|
+
warnings: [
|
|
102
|
+
"This removes the binary only. Anything it wrote to your home directory — config, caches, credentials — stays.",
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The plan as a line someone can read before agreeing to it. */
|
|
108
|
+
export function describeUninstall(plan) {
|
|
109
|
+
return plan.steps
|
|
110
|
+
.map((s) => (s.kind === "run" ? `run ${s.command} ${s.args.join(" ")}` : `remove ${s.path}`))
|
|
111
|
+
.concat((plan.warnings || []).map((w) => `note ${w}`))
|
|
112
|
+
.join("\n");
|
|
113
|
+
}
|
package/src/upgrade.mjs
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// `moshcode upgrade` — update everything that has a newer version: moshcode,
|
|
2
|
+
// installed coding engines, and installed workflow tools. Conductor pattern:
|
|
3
|
+
// re-run each target's own updater/installer (they fetch latest), never vendor.
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import { ENGINES, engineStatus, exitReason, ranOk, resolveEngine, upgradeSpec, runCmd } from "./engines.mjs";
|
|
8
|
+
import { TOOLS, resolveTool, toolStatus, toolUpgradeSpec } from "./tools.mjs";
|
|
9
|
+
|
|
10
|
+
// Self-upgrade re-runs the moshcode installer's `update` path. Defaults to the
|
|
11
|
+
// GitHub-hosted install.sh (always live); override with MOSHCODE_INSTALL_URL.
|
|
12
|
+
const SELF_URL = process.env.MOSHCODE_INSTALL_URL
|
|
13
|
+
|| "https://raw.githubusercontent.com/moshcoder/moshcode/main/install.sh";
|
|
14
|
+
|
|
15
|
+
// Where the *running* moshcode actually lives (…/<home>/src/upgrade.mjs → <home>).
|
|
16
|
+
// We point the installer at this so it updates THIS copy in place, not a default
|
|
17
|
+
// path that might not be the one on your PATH.
|
|
18
|
+
export const MOSHCODE_HOME = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
|
|
20
|
+
// Running from a git checkout? Then a reinstall would blow away the working tree
|
|
21
|
+
// — update it with `git pull`, not the installer.
|
|
22
|
+
function isGitCheckout(dir) {
|
|
23
|
+
return fs.existsSync(path.join(dir, ".git"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function selfVersion() {
|
|
27
|
+
try { return JSON.parse(fs.readFileSync(path.join(MOSHCODE_HOME, "package.json"), "utf8")).version || null; }
|
|
28
|
+
catch { return null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// POSIX-safe single-quoting for values embedded in the `sh -c` command line:
|
|
32
|
+
// wrap in '…' and escape every ' as '\'' . Without this an apostrophe in the
|
|
33
|
+
// install path (e.g. /Users/o'brien/moshcode) broke self-upgrade with a shell
|
|
34
|
+
// syntax error — and a hostile path could break out of the quotes and inject
|
|
35
|
+
// extra shell into the update command.
|
|
36
|
+
const shQ = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
37
|
+
|
|
38
|
+
export function selfSpec(home = MOSHCODE_HOME, url = SELF_URL) {
|
|
39
|
+
// Export MOSHCODE_HOME so install.sh updates the exact dir we run from.
|
|
40
|
+
return { cmd: "sh", args: ["-c", `export MOSHCODE_HOME=${shQ(home)}; curl -fsSL ${shQ(url)} | sh -s -- update`] };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Work out an upgrade plan from optional targets:
|
|
45
|
+
* []/["all"] → moshcode + every installed engine and tool
|
|
46
|
+
* ["self"|"moshcode"] → moshcode only
|
|
47
|
+
* ["engines"] → all installed engines (no self)
|
|
48
|
+
* ["tools"] → all installed tools (no self)
|
|
49
|
+
* ["claude"|"ugig", …] → named targets (install if not present yet)
|
|
50
|
+
* Returns { self, items:[{key,label,kind,spec,installed}], unknown:[] }.
|
|
51
|
+
*/
|
|
52
|
+
export function planUpgrade(targets = []) {
|
|
53
|
+
const t = targets.map((x) => String(x).trim().toLowerCase()).filter(Boolean);
|
|
54
|
+
const engines = engineStatus();
|
|
55
|
+
const tools = toolStatus();
|
|
56
|
+
const engineByKey = Object.fromEntries(engines.map((entry) => [entry.key, entry]));
|
|
57
|
+
const toolByKey = Object.fromEntries(tools.map((entry) => [entry.key, entry]));
|
|
58
|
+
|
|
59
|
+
const wantsAll = t.length === 0 || t.includes("all");
|
|
60
|
+
const wantsSelf = wantsAll || t.includes("self") || t.includes("moshcode");
|
|
61
|
+
const wantsEngines = wantsAll || t.includes("engines");
|
|
62
|
+
const wantsTools = wantsAll || t.includes("tools");
|
|
63
|
+
|
|
64
|
+
const items = [];
|
|
65
|
+
const unknown = [];
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
|
|
68
|
+
// A native updater (`doppler update`, `aider --upgrade`) is the missing
|
|
69
|
+
// binary itself, so it cannot be what installs it. When the target is not
|
|
70
|
+
// present yet, run its installer — that is the command the "(installing —
|
|
71
|
+
// not present)" note below promises. Installed targets keep the native
|
|
72
|
+
// updater, which is the whole point of having one.
|
|
73
|
+
const addEngine = (key) => {
|
|
74
|
+
const id = `engine:${key}`;
|
|
75
|
+
if (seen.has(id)) return;
|
|
76
|
+
seen.add(id);
|
|
77
|
+
const installed = engineByKey[key].installed;
|
|
78
|
+
items.push({
|
|
79
|
+
key,
|
|
80
|
+
label: key,
|
|
81
|
+
kind: "engine",
|
|
82
|
+
spec: installed ? upgradeSpec(ENGINES[key]) : ENGINES[key].install,
|
|
83
|
+
// Where to turn when a native updater refuses. Only set when the updater
|
|
84
|
+
// is something other than the installer, so a fallback can never repeat
|
|
85
|
+
// the command that just failed.
|
|
86
|
+
fallback: installed && upgradeSpec(ENGINES[key]) !== ENGINES[key].install ? ENGINES[key].install : null,
|
|
87
|
+
installed,
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
const addTool = (key) => {
|
|
91
|
+
const id = `tool:${key}`;
|
|
92
|
+
if (seen.has(id)) return;
|
|
93
|
+
seen.add(id);
|
|
94
|
+
const installed = toolByKey[key].installed;
|
|
95
|
+
items.push({
|
|
96
|
+
key,
|
|
97
|
+
label: key,
|
|
98
|
+
kind: "tool",
|
|
99
|
+
spec: installed ? toolUpgradeSpec(TOOLS[key]) : TOOLS[key].install,
|
|
100
|
+
fallback: installed && toolUpgradeSpec(TOOLS[key]) !== TOOLS[key].install ? TOOLS[key].install : null,
|
|
101
|
+
installed,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (wantsEngines) {
|
|
106
|
+
for (const engine of engines) if (engine.installed) addEngine(engine.key);
|
|
107
|
+
}
|
|
108
|
+
if (wantsTools) {
|
|
109
|
+
for (const tool of tools) if (tool.installed) addTool(tool.key);
|
|
110
|
+
}
|
|
111
|
+
// Explicit names/engine aliases upgrade even when not currently installed.
|
|
112
|
+
for (const tok of t) {
|
|
113
|
+
if (["all", "self", "moshcode", "engines", "tools"].includes(tok)) continue;
|
|
114
|
+
const engine = resolveEngine(tok);
|
|
115
|
+
const tool = resolveTool(tok);
|
|
116
|
+
if (engine) addEngine(engine[0]);
|
|
117
|
+
else if (tool) addTool(tool[0]);
|
|
118
|
+
else unknown.push(tok);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { self: wantsSelf, items, unknown };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Run an upgrade plan sequentially, streaming each tool's own output. `io.log`
|
|
126
|
+
* prints a status line, `io.rule` draws a divider around each hand-off (both
|
|
127
|
+
* optional — default to plain console output). Returns a summary array.
|
|
128
|
+
*/
|
|
129
|
+
export async function runUpgrade(targets = [], io = {}) {
|
|
130
|
+
const log = io.log || ((s) => console.log(s));
|
|
131
|
+
const rule = io.rule || (() => console.log("─".repeat(48)));
|
|
132
|
+
|
|
133
|
+
// `sudo moshcode update` is the one escalation this CLI must never accept.
|
|
134
|
+
// selfSpec re-runs the installer, and every path the installer uses comes
|
|
135
|
+
// from $HOME — which sudo has set to /root. The update "succeeds", moshcode
|
|
136
|
+
// is reinstalled into root's home, and the operator is left with a binary on
|
|
137
|
+
// PATH they cannot execute. Engine and tool installers have the same shape:
|
|
138
|
+
// they write into $HOME too.
|
|
139
|
+
//
|
|
140
|
+
// A bare root shell has no SUDO_USER and is a legitimate place to run this,
|
|
141
|
+
// so only the escalated-from-a-real-user case is refused.
|
|
142
|
+
const uid = io.uid ?? (typeof process.getuid === "function" ? process.getuid() : 0);
|
|
143
|
+
const env = io.env || process.env;
|
|
144
|
+
if (uid === 0 && env.SUDO_USER && !env.MOSHCODE_ALLOW_ROOT) {
|
|
145
|
+
log(`✗ don't run moshcode update with sudo.`);
|
|
146
|
+
log("");
|
|
147
|
+
log(` It reinstalls moshcode, and the installer puts everything under $HOME —`);
|
|
148
|
+
log(` which sudo has set to ${env.HOME || "/root"}. That would install moshcode for`);
|
|
149
|
+
log(` root and leave ${env.SUDO_USER} with a moshcode on PATH it cannot execute.`);
|
|
150
|
+
log("");
|
|
151
|
+
log(" Run it as yourself instead:");
|
|
152
|
+
log(" moshcode update");
|
|
153
|
+
log("");
|
|
154
|
+
log(" Commands that genuinely need root, like `dns enable`, now ask for it");
|
|
155
|
+
log(" themselves — you do not need to escalate the whole CLI for those.");
|
|
156
|
+
return [{ name: "moshcode", ok: false, code: 1, signal: null }];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const { self, items, unknown } = planUpgrade(targets);
|
|
160
|
+
|
|
161
|
+
for (const u of unknown) log(`? skipping unknown upgrade target "${u}"`);
|
|
162
|
+
|
|
163
|
+
if (!self && items.length === 0) {
|
|
164
|
+
if (!unknown.length) log("nothing to upgrade — no matching engines or tools are installed.");
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const exec = io.runCmd || runCmd;
|
|
169
|
+
|
|
170
|
+
const results = [];
|
|
171
|
+
const attempt = async (name, spec, note) => {
|
|
172
|
+
log(`\n⬆ upgrading ${name}${note ? ` ${note}` : ""} — ${spec.cmd} ${spec.args.join(" ")}`);
|
|
173
|
+
rule();
|
|
174
|
+
const r = await exec(spec.cmd, spec.args);
|
|
175
|
+
rule();
|
|
176
|
+
const ok = ranOk(r);
|
|
177
|
+
log(ok ? `✓ ${name} up to date` : `✗ ${name} upgrade failed (${exitReason(r)})`);
|
|
178
|
+
return { name, ok, code: r.code, signal: r.signal ?? null };
|
|
179
|
+
};
|
|
180
|
+
const run = async (name, spec, note) => {
|
|
181
|
+
const result = await attempt(name, spec, note);
|
|
182
|
+
results.push(result);
|
|
183
|
+
return result.ok;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (self) {
|
|
187
|
+
if (isGitCheckout(MOSHCODE_HOME)) {
|
|
188
|
+
// Don't reinstall over a working tree — just tell the user how to update it.
|
|
189
|
+
log(`\n· moshcode runs from a git checkout (${MOSHCODE_HOME}) — \`git pull\` there to update it (skipping self-reinstall).`);
|
|
190
|
+
} else {
|
|
191
|
+
const before = selfVersion();
|
|
192
|
+
const ok = await run("moshcode", selfSpec(), "(self)");
|
|
193
|
+
const after = selfVersion();
|
|
194
|
+
if (ok && before && after) {
|
|
195
|
+
log(before === after ? `· moshcode already at ${after}` : `· moshcode ${before} → ${after} — restart moshcode to load it.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const it of items) {
|
|
200
|
+
const result = await attempt(it.label, it.spec, it.installed ? "" : "(installing — not present)");
|
|
201
|
+
// A native updater that can't tell how the binary got there fails the same
|
|
202
|
+
// way on every run — an opencode fork living under its own directory, a
|
|
203
|
+
// binary someone moved, a machine where the installer left no marker. The
|
|
204
|
+
// installer is idempotent and fetches the latest, so reach for it rather
|
|
205
|
+
// than leaving the target stranded on an old version.
|
|
206
|
+
if (!result.ok && it.fallback) {
|
|
207
|
+
log(`· ${it.label}'s own updater could not do it — falling back to its installer`);
|
|
208
|
+
results.push(await attempt(it.label, it.fallback, "(installer)"));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
results.push(result);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const failed = results.filter((r) => !r.ok);
|
|
215
|
+
log(`\n${failed.length ? "✗" : "✓"} upgraded ${results.length - failed.length}/${results.length}${failed.length ? ` — failed: ${failed.map((r) => r.name).join(", ")}` : "."} 🤘`);
|
|
216
|
+
return results;
|
|
217
|
+
}
|