@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.
- package/CHANGELOG.md +75 -137
- package/README.md +37 -20
- package/package.json +14 -12
- package/src/Browser.tsx +331 -226
- package/src/CommandPalette.tsx +67 -46
- package/src/Footer.tsx +71 -27
- package/src/Header.tsx +68 -0
- package/src/HelpOverlay.tsx +59 -41
- package/src/PromptRow.tsx +49 -0
- package/src/brand.ts +7 -0
- package/src/cli/argv.ts +34 -3
- package/src/commands/buildCommands.ts +1 -0
- package/src/config/load.ts +84 -19
- package/src/discovery/walk.ts +8 -3
- package/src/index.tsx +64 -11
- package/src/io/editor.ts +162 -0
- package/src/keymap/browser.ts +79 -22
- package/src/keymap/keymap.ts +6 -0
- package/src/layout/resolve.ts +9 -9
- package/src/layout/sidebarRow.ts +85 -0
- package/src/serve/server.ts +4 -1
- package/src/theme/colors.ts +51 -15
- package/src/theme/types.ts +7 -0
- package/src/update/cache.ts +77 -0
- package/src/update/check.ts +165 -0
- package/src/update/compare.ts +41 -0
- package/src/update/notice.ts +29 -0
- package/src/update/runtime.ts +48 -0
- package/src/update/useUpdateNotice.ts +24 -0
|
@@ -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
|
+
}
|