@carlesandres/house 0.4.0 → 0.4.2

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.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Strict-greater version compare for the update notice.
3
+ *
4
+ * The notice fires only when the registry's published version is *strictly
5
+ * greater* than the running version on its numeric base. Pre-release
6
+ * suffixes are stripped before compare because:
7
+ *
8
+ * - `dist-tags.latest` is by convention a stable, never a pre-release; the
9
+ * notice does not target users who opted into pre-releases via a custom
10
+ * install command.
11
+ * - A local dev build of `0.5.0-dev.3` should NOT be nagged toward the
12
+ * published `0.5.0` while iterating on the same base — they're "the
13
+ * same version" for nag purposes.
14
+ */
15
+
16
+ const toBaseSegments = (version: string): readonly number[] | null => {
17
+ const base = version.split("-", 1)[0] ?? ""
18
+ const parts = base.split(".")
19
+ const nums: number[] = []
20
+ for (const p of parts) {
21
+ const n = Number(p)
22
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) return null
23
+ nums.push(n)
24
+ }
25
+ return nums.length > 0 ? nums : null
26
+ }
27
+
28
+ /** True iff `candidate` > `current` on the numeric base. Malformed input → false. */
29
+ export const isNewer = (candidate: string, current: string): boolean => {
30
+ const a = toBaseSegments(candidate)
31
+ const b = toBaseSegments(current)
32
+ if (!a || !b) return false
33
+ const len = Math.max(a.length, b.length)
34
+ for (let i = 0; i < len; i++) {
35
+ const x = a[i] ?? 0
36
+ const y = b[i] ?? 0
37
+ if (x > y) return true
38
+ if (x < y) return false
39
+ }
40
+ return false
41
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Notice formatting for the update check. Kept separate from the probe so
3
+ * the surfaces (in-app footer, quit-time stderr print) can be reshaped
4
+ * without touching the network or cache code.
5
+ *
6
+ * Install-method note: we don't try to detect npm vs bun vs Homebrew. The
7
+ * runtime probe (`process.versions.bun`) only tells us how house was
8
+ * launched, not how it was installed — a user who ran `npm i -g …` and
9
+ * then happens to invoke via a bun-installed shim would be misled.
10
+ * Showing both commands is unambiguous and lets the user pick the one
11
+ * matching their install.
12
+ */
13
+
14
+ import type { UpdateInfo } from "./check.ts"
15
+
16
+ /** One-liner for the footer toast — must fit a tight viewport. */
17
+ export const formatFooterNotice = (info: UpdateInfo): string =>
18
+ `update available: ${info.latestVersion} (current ${info.currentVersion})`
19
+
20
+ /** Multi-line block printed to stderr after the renderer tears down. The
21
+ * user keeps this in scrollback and can copy the command directly. */
22
+ export const formatQuitNotice = (info: UpdateInfo): string =>
23
+ [
24
+ "",
25
+ `house ${info.latestVersion} is available (you have ${info.currentVersion}).`,
26
+ ` npm i -g ${info.pkgName}`,
27
+ ` bun add -g ${info.pkgName}`,
28
+ "",
29
+ ].join("\n")
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Process-singleton wiring for the update probe.
3
+ *
4
+ * The probe runs at most once per process. The result lands in a module
5
+ * variable so:
6
+ * - any UI surface (DiscoverShell, App) can subscribe and re-render when
7
+ * it resolves, without each one issuing its own request;
8
+ * - the `process.on("exit")` hook can synchronously read the result and
9
+ * print the quit-time notice (async work can't run during 'exit').
10
+ *
11
+ * Failures resolve to `null` — silent by design. See `check.ts`.
12
+ */
13
+
14
+ import type { UpdateInfo } from "./check.ts"
15
+ import { checkForUpdate } from "./check.ts"
16
+
17
+ let current: UpdateInfo | null = null
18
+ let started = false
19
+ let pending: Promise<void> | null = null
20
+ const listeners = new Set<(info: UpdateInfo) => void>()
21
+
22
+ export const startUpdateProbe = (pkgName: string, currentVersion: string): Promise<void> => {
23
+ if (pending) return pending
24
+ started = true
25
+ pending = checkForUpdate({ pkgName, currentVersion })
26
+ .then((info) => {
27
+ if (info) {
28
+ current = info
29
+ for (const cb of listeners) cb(info)
30
+ }
31
+ })
32
+ .catch(() => {
33
+ // silent — the feature is opportunistic
34
+ })
35
+ return pending
36
+ }
37
+
38
+ export const currentUpdateInfo = (): UpdateInfo | null => current
39
+
40
+ export const subscribeUpdateInfo = (cb: (info: UpdateInfo) => void): (() => void) => {
41
+ if (current) cb(current)
42
+ listeners.add(cb)
43
+ return () => {
44
+ listeners.delete(cb)
45
+ }
46
+ }
47
+
48
+ export const isProbeStarted = (): boolean => started
@@ -0,0 +1,24 @@
1
+ /**
2
+ * React hook for the update-available footer toast.
3
+ *
4
+ * Returns the formatted one-liner once the singleton probe resolves with a
5
+ * strictly-newer-and-downloadable version, and `null` otherwise. Mounting
6
+ * multiple consumers is safe — they all share the same probe via
7
+ * `runtime.ts`.
8
+ */
9
+
10
+ import { useEffect, useState } from "react"
11
+ import { currentUpdateInfo, subscribeUpdateInfo } from "./runtime.ts"
12
+ import { formatFooterNotice } from "./notice.ts"
13
+
14
+ export const useUpdateNotice = (): string | null => {
15
+ const [text, setText] = useState<string | null>(() => {
16
+ const cur = currentUpdateInfo()
17
+ return cur ? formatFooterNotice(cur) : null
18
+ })
19
+ useEffect(() => {
20
+ const unsub = subscribeUpdateInfo((info) => setText(formatFooterNotice(info)))
21
+ return unsub
22
+ }, [])
23
+ return text
24
+ }