mulmoterminal 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -24,6 +24,10 @@ npm install -g mulmoterminal
24
24
  mulmoterminal
25
25
  ```
26
26
 
27
+ A global install isn't auto-updated, so on startup MulmoTerminal checks npm and
28
+ prints a one-line notice when a newer version is available (`npm i -g mulmoterminal`
29
+ to update). Disable with `MULMOTERMINAL_NO_UPDATE_CHECK=1` (or `NO_UPDATE_NOTIFIER=1`).
30
+
27
31
  Options: `--cwd <dir>` (working directory — relative paths allowed; defaults to the
28
32
  directory you run the command from), `--port <n>` (default 3456), `--no-open`,
29
33
  `--version`, `--help`.
@@ -12,6 +12,7 @@ import { createRequire } from "node:module";
12
12
  import { createServer } from "node:net";
13
13
  import { dirname, join, resolve } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
+ import { fetchLatestVersion, isNewerVersion } from "./update-check.js";
15
16
 
16
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
18
  const PKG_DIR = join(__dirname, "..");
@@ -30,6 +31,21 @@ const { version: VERSION } = createRequire(import.meta.url)("../package.json");
30
31
  const log = (msg) => console.log(`\x1b[36m[mulmoterminal]\x1b[0m ${msg}`);
31
32
  const error = (msg) => console.error(`\x1b[31m[mulmoterminal]\x1b[0m ${msg}`);
32
33
 
34
+ // Non-blocking notice when a newer version is published — `npm i -g` never
35
+ // auto-updates. Opt out via MULMOTERMINAL_NO_UPDATE_CHECK / NO_UPDATE_NOTIFIER.
36
+ function checkForUpdate() {
37
+ if (process.env.MULMOTERMINAL_NO_UPDATE_CHECK || process.env.NO_UPDATE_NOTIFIER) return;
38
+ fetchLatestVersion()
39
+ .then((latest) => {
40
+ if (latest && isNewerVersion(latest, VERSION)) {
41
+ log(`\x1b[33mUpdate available: ${VERSION} → ${latest} · run: npm i -g mulmoterminal\x1b[0m`);
42
+ }
43
+ })
44
+ .catch(() => {
45
+ // best-effort; never disrupt startup
46
+ });
47
+ }
48
+
33
49
  function claudeInstalled() {
34
50
  try {
35
51
  // Intentionally resolves `claude` from the user's PATH — detecting their
@@ -230,6 +246,8 @@ Options:
230
246
  return;
231
247
  }
232
248
 
249
+ checkForUpdate();
250
+
233
251
  if (!claudeInstalled()) {
234
252
  error("Claude Code CLI not found.");
235
253
  error("Install it first: npm install -g @anthropic-ai/claude-code && claude auth login");
@@ -0,0 +1,37 @@
1
+ // Update-check helpers for the launcher, split out so the version comparison is
2
+ // unit-testable. Network calls are best-effort and never throw.
3
+
4
+ const REGISTRY = (process.env.npm_config_registry || "https://registry.npmjs.org").replace(/\/$/, "");
5
+
6
+ // Best-effort latest-version lookup. Resolves null on any failure (offline,
7
+ // timeout, non-OK, bad payload) so callers never block or break startup.
8
+ export async function fetchLatestVersion(pkg = "mulmoterminal") {
9
+ try {
10
+ const res = await fetch(`${REGISTRY}/${pkg}/latest`, {
11
+ signal: AbortSignal.timeout(1500),
12
+ headers: { accept: "application/json" },
13
+ });
14
+ if (!res.ok) return null;
15
+ const body = await res.json();
16
+ return typeof body.version === "string" ? body.version : null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ // True if `latest` is a strictly newer major.minor.patch than `current`.
23
+ // Pre-release suffixes are ignored and parts are compared numerically (so
24
+ // 0.1.10 > 0.1.9, which a lexical compare would get wrong).
25
+ export function isNewerVersion(latest, current) {
26
+ const parts = (v) =>
27
+ String(v)
28
+ .split("-")[0]
29
+ .split(".")
30
+ .map((n) => Number.parseInt(n, 10) || 0);
31
+ const a = parts(latest);
32
+ const b = parts(current);
33
+ for (let i = 0; i < 3; i++) {
34
+ if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
35
+ }
36
+ return false;
37
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { isNewerVersion, fetchLatestVersion } from "./update-check.js";
3
+
4
+ describe("isNewerVersion", () => {
5
+ const cases: [string, string, boolean][] = [
6
+ ["0.1.3", "0.1.0", true],
7
+ ["0.2.0", "0.1.9", true],
8
+ ["1.0.0", "0.9.9", true],
9
+ ["0.1.10", "0.1.9", true], // numeric, not lexical (the bug a string compare hits)
10
+ ["0.1.3", "0.1.3", false],
11
+ ["0.1.0", "0.1.3", false],
12
+ ["0.9.9", "1.0.0", false],
13
+ ["0.1.4-beta.1", "0.1.3", true], // pre-release suffix ignored on the core
14
+ ["0.1.3", "0.1.3-beta.1", false], // equal core → not newer
15
+ ];
16
+ it.each(cases)("isNewerVersion(%s, %s) === %s", (latest, current, expected) => {
17
+ expect(isNewerVersion(latest, current)).toBe(expected);
18
+ });
19
+ });
20
+
21
+ describe("fetchLatestVersion", () => {
22
+ const stubFetch = (impl: () => Promise<unknown>) => vi.stubGlobal("fetch", vi.fn(impl));
23
+ afterEach(() => vi.unstubAllGlobals());
24
+
25
+ it("returns the version from a 200 response", async () => {
26
+ stubFetch(async () => ({ ok: true, json: async () => ({ version: "1.2.3" }) }));
27
+ expect(await fetchLatestVersion()).toBe("1.2.3");
28
+ });
29
+
30
+ it("returns null on a non-OK response", async () => {
31
+ stubFetch(async () => ({ ok: false, json: async () => ({}) }));
32
+ expect(await fetchLatestVersion()).toBeNull();
33
+ });
34
+
35
+ it("returns null when fetch rejects (offline / timeout)", async () => {
36
+ stubFetch(async () => {
37
+ throw new Error("offline");
38
+ });
39
+ expect(await fetchLatestVersion()).toBeNull();
40
+ });
41
+
42
+ it("returns null when the payload has no version string", async () => {
43
+ stubFetch(async () => ({ ok: true, json: async () => ({ name: "mulmoterminal" }) }));
44
+ expect(await fetchLatestVersion()).toBeNull();
45
+ });
46
+ });