@carlesandres/house 0.4.13 → 0.5.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +32 -2
  2. package/README.md +10 -8
  3. package/dist/bin.js +92 -0
  4. package/dist/index.js +10005 -0
  5. package/package.json +15 -3
  6. package/src/Browser.tsx +0 -1210
  7. package/src/CommandPalette.tsx +0 -214
  8. package/src/Footer.tsx +0 -258
  9. package/src/Header.tsx +0 -71
  10. package/src/PromptRow.tsx +0 -51
  11. package/src/Spinner.tsx +0 -39
  12. package/src/StatusIndicator.tsx +0 -55
  13. package/src/StatusPopover.tsx +0 -166
  14. package/src/brand.ts +0 -7
  15. package/src/cli/argv.ts +0 -179
  16. package/src/commands/buildCommands.ts +0 -98
  17. package/src/commands/paletteOnlyCommands.ts +0 -27
  18. package/src/commands/score.ts +0 -78
  19. package/src/commands/types.ts +0 -26
  20. package/src/config/load.ts +0 -381
  21. package/src/config/save.ts +0 -95
  22. package/src/discovery/filter.ts +0 -148
  23. package/src/discovery/show.ts +0 -48
  24. package/src/discovery/walk.ts +0 -209
  25. package/src/index.tsx +0 -464
  26. package/src/io/clipboard.ts +0 -53
  27. package/src/io/editor.ts +0 -162
  28. package/src/io/readFile.ts +0 -14
  29. package/src/keymap/browser.ts +0 -335
  30. package/src/keymap/displayKey.ts +0 -17
  31. package/src/keymap/keymap.ts +0 -95
  32. package/src/layout/resolve.ts +0 -52
  33. package/src/layout/sidebarEmptyState.ts +0 -7
  34. package/src/layout/sidebarRow.ts +0 -69
  35. package/src/markdown/frontmatter.ts +0 -62
  36. package/src/serve/css.ts +0 -120
  37. package/src/serve/openBrowser.ts +0 -19
  38. package/src/serve/render.ts +0 -56
  39. package/src/serve/server.ts +0 -166
  40. package/src/theme/atom.ts +0 -23
  41. package/src/theme/colors.ts +0 -86
  42. package/src/theme/loader.ts +0 -110
  43. package/src/theme/registry.ts +0 -12
  44. package/src/theme/resolve.ts +0 -168
  45. package/src/theme/themes/aura.json +0 -58
  46. package/src/theme/themes/ayu.json +0 -69
  47. package/src/theme/themes/carbonfox.json +0 -201
  48. package/src/theme/themes/catppuccin-frappe.json +0 -186
  49. package/src/theme/themes/catppuccin-macchiato.json +0 -186
  50. package/src/theme/themes/catppuccin.json +0 -212
  51. package/src/theme/themes/cobalt2.json +0 -181
  52. package/src/theme/themes/cursor.json +0 -202
  53. package/src/theme/themes/dracula.json +0 -172
  54. package/src/theme/themes/everforest.json +0 -194
  55. package/src/theme/themes/flexoki.json +0 -190
  56. package/src/theme/themes/github.json +0 -186
  57. package/src/theme/themes/gruvbox.json +0 -195
  58. package/src/theme/themes/kanagawa.json +0 -180
  59. package/src/theme/themes/lucent-orng.json +0 -186
  60. package/src/theme/themes/material.json +0 -188
  61. package/src/theme/themes/matrix.json +0 -180
  62. package/src/theme/themes/mercury.json +0 -198
  63. package/src/theme/themes/monokai.json +0 -174
  64. package/src/theme/themes/nightowl.json +0 -174
  65. package/src/theme/themes/nord.json +0 -176
  66. package/src/theme/themes/one-dark.json +0 -184
  67. package/src/theme/themes/opencode.json +0 -198
  68. package/src/theme/themes/orng.json +0 -202
  69. package/src/theme/themes/osaka-jade.json +0 -193
  70. package/src/theme/themes/palenight.json +0 -175
  71. package/src/theme/themes/rosepine.json +0 -187
  72. package/src/theme/themes/solarized.json +0 -180
  73. package/src/theme/themes/synthwave84.json +0 -179
  74. package/src/theme/themes/tokyonight.json +0 -196
  75. package/src/theme/themes/vercel.json +0 -198
  76. package/src/theme/themes/vesper.json +0 -171
  77. package/src/theme/themes/zenburn.json +0 -176
  78. package/src/theme/types.ts +0 -115
  79. package/src/tips.ts +0 -88
  80. package/src/ui/middleTruncate.ts +0 -27
  81. package/src/update/cache.ts +0 -77
  82. package/src/update/check.ts +0 -165
  83. package/src/update/compare.ts +0 -41
  84. package/src/update/notice.ts +0 -29
  85. package/src/update/runtime.ts +0 -48
  86. package/src/update/useUpdateNotice.ts +0 -24
@@ -1,165 +0,0 @@
1
- /**
2
- * Update probe. Resolves to an upgrade record iff a strictly-newer version
3
- * is published on npm AND its tarball is actually downloadable.
4
- *
5
- * The HEAD on `dist.tarball` is the load-bearing step: the npm registry can
6
- * publish version metadata moments before the CDN serves the tarball, and
7
- * we promised not to nag the user toward a version they cannot install
8
- * yet. A non-200 HEAD invalidates the cache entry (tarballOk=false) so the
9
- * next launch retries instead of waiting out the TTL.
10
- *
11
- * All failures are silent. The notice surface is opportunistic — the user
12
- * should never see an error from a feature whose job is to whisper.
13
- */
14
-
15
- import { isNewer } from "./compare.ts"
16
- import { readCache, writeCache, type UpdateCacheRecord } from "./cache.ts"
17
-
18
- export const TTL_MS = 60 * 60 * 1000
19
- const FETCH_TIMEOUT_MS = 3000
20
- const REGISTRY_URL = (pkgName: string) => `https://registry.npmjs.org/${pkgName}/latest`
21
-
22
- export interface UpdateInfo {
23
- readonly pkgName: string
24
- readonly currentVersion: string
25
- readonly latestVersion: string
26
- }
27
-
28
- export interface CheckOptions {
29
- readonly pkgName: string
30
- readonly currentVersion: string
31
- /** Override for tests. Defaults to the npm registry + global fetch. */
32
- readonly now?: () => number
33
- readonly env?: Record<string, string | undefined>
34
- readonly fetchImpl?: typeof fetch
35
- readonly cacheRead?: () => Promise<UpdateCacheRecord | null>
36
- readonly cacheWrite?: (r: UpdateCacheRecord) => Promise<void>
37
- }
38
-
39
- /** Treat any non-empty, non-`0`, non-`false` value as opt-out. Mirrors the
40
- * permissiveness of the de-facto npm-ecosystem convention; a user copy-
41
- * pasting `NO_UPDATE_NOTIFIER=true` from another tool's docs should work. */
42
- const isTruthyEnv = (value: string | undefined): boolean => {
43
- if (!value) return false
44
- const v = value.toLowerCase()
45
- return v !== "0" && v !== "false" && v !== "no"
46
- }
47
-
48
- /** Most major CIs (GitHub Actions, GitLab, Travis, CircleCI, Jenkins) set
49
- * `CI=true`. A few set it to other truthy values; treat any non-empty
50
- * value as "we're in CI, skip the nag." */
51
- const isCi = (env: Record<string, string | undefined>): boolean =>
52
- typeof env.CI === "string" &&
53
- env.CI.length > 0 &&
54
- env.CI !== "0" &&
55
- env.CI.toLowerCase() !== "false"
56
-
57
- /** Run a fetch with a single timeout that covers BOTH the response headers
58
- * and the caller's body read. Returning the bare Response and then reading
59
- * `res.json()` outside the timer leaves the body stream unbounded — a
60
- * stalled connection after headers would hang forever. The consumer
61
- * callback runs while the AbortController is still live, so an abort
62
- * cancels an in-flight body read too. */
63
- const fetchWithTimeout = async <T>(
64
- url: string,
65
- init: RequestInit,
66
- consume: (res: Response) => Promise<T>,
67
- fetchImpl: typeof fetch = fetch,
68
- ): Promise<T> => {
69
- const ctrl = new AbortController()
70
- const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS)
71
- try {
72
- const res = await fetchImpl(url, { ...init, signal: ctrl.signal })
73
- return await consume(res)
74
- } finally {
75
- clearTimeout(timer)
76
- }
77
- }
78
-
79
- /**
80
- * Run the probe and return upgrade info if one applies. `null` covers
81
- * every "no nag" case: opt-out, recent cache hit on the same version, no
82
- * newer version, registry/CDN failure, malformed response.
83
- */
84
- export const checkForUpdate = async (opts: CheckOptions): Promise<UpdateInfo | null> => {
85
- const env = opts.env ?? process.env
86
- if (isTruthyEnv(env.NO_UPDATE_NOTIFIER) || isCi(env)) {
87
- return null
88
- }
89
-
90
- const now = opts.now ?? Date.now
91
- const cacheRead = opts.cacheRead ?? (() => readCache())
92
- const cacheWrite = opts.cacheWrite ?? ((r: UpdateCacheRecord) => writeCache(r))
93
-
94
- const cached = await cacheRead()
95
- const fresh = cached !== null && cached.tarballOk && now() - cached.checkedAt < TTL_MS
96
- if (fresh) {
97
- return isNewer(cached.latestVersion, opts.currentVersion)
98
- ? {
99
- pkgName: opts.pkgName,
100
- currentVersion: opts.currentVersion,
101
- latestVersion: cached.latestVersion,
102
- }
103
- : null
104
- }
105
-
106
- // Probe the registry.
107
- let latestVersion: string
108
- let tarballUrl: string
109
- try {
110
- const parsed = await fetchWithTimeout(
111
- REGISTRY_URL(opts.pkgName),
112
- { headers: { accept: "application/json" } },
113
- async (res) => {
114
- if (!res.ok) return null
115
- const body = (await res.json()) as unknown
116
- if (typeof body !== "object" || body === null) return null
117
- const obj = body as Record<string, unknown>
118
- const version = obj.version
119
- const dist = obj.dist as Record<string, unknown> | undefined
120
- const tarball = dist?.tarball
121
- if (typeof version !== "string" || typeof tarball !== "string") return null
122
- // The registry returns a CDN URL we're about to HEAD without
123
- // further validation. Pin to https so a compromised or proxied
124
- // registry can't redirect us to file://, http://, or another
125
- // scheme we'd issue a request against.
126
- try {
127
- if (new URL(tarball).protocol !== "https:") return null
128
- } catch {
129
- return null
130
- }
131
- return { version, tarball }
132
- },
133
- opts.fetchImpl,
134
- )
135
- if (!parsed) return null
136
- latestVersion = parsed.version
137
- tarballUrl = parsed.tarball
138
- } catch {
139
- return null
140
- }
141
-
142
- // Verify the artifact is actually downloadable. This is the step that
143
- // makes "only announced when the artifact is available" hold.
144
- let tarballOk = false
145
- try {
146
- tarballOk = await fetchWithTimeout(
147
- tarballUrl,
148
- { method: "HEAD" },
149
- async (res) => res.ok,
150
- opts.fetchImpl,
151
- )
152
- } catch {
153
- tarballOk = false
154
- }
155
-
156
- await cacheWrite({ checkedAt: now(), latestVersion, tarballOk })
157
-
158
- if (!tarballOk) return null
159
- if (!isNewer(latestVersion, opts.currentVersion)) return null
160
- return {
161
- pkgName: opts.pkgName,
162
- currentVersion: opts.currentVersion,
163
- latestVersion,
164
- }
165
- }
@@ -1,41 +0,0 @@
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
- }
@@ -1,29 +0,0 @@
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")
@@ -1,48 +0,0 @@
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 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
@@ -1,24 +0,0 @@
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
- }