@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.
- package/CHANGELOG.md +32 -2
- package/README.md +10 -8
- package/dist/bin.js +92 -0
- package/dist/index.js +10005 -0
- package/package.json +15 -3
- package/src/Browser.tsx +0 -1210
- package/src/CommandPalette.tsx +0 -214
- package/src/Footer.tsx +0 -258
- package/src/Header.tsx +0 -71
- package/src/PromptRow.tsx +0 -51
- package/src/Spinner.tsx +0 -39
- package/src/StatusIndicator.tsx +0 -55
- package/src/StatusPopover.tsx +0 -166
- package/src/brand.ts +0 -7
- package/src/cli/argv.ts +0 -179
- package/src/commands/buildCommands.ts +0 -98
- package/src/commands/paletteOnlyCommands.ts +0 -27
- package/src/commands/score.ts +0 -78
- package/src/commands/types.ts +0 -26
- package/src/config/load.ts +0 -381
- package/src/config/save.ts +0 -95
- package/src/discovery/filter.ts +0 -148
- package/src/discovery/show.ts +0 -48
- package/src/discovery/walk.ts +0 -209
- package/src/index.tsx +0 -464
- package/src/io/clipboard.ts +0 -53
- package/src/io/editor.ts +0 -162
- package/src/io/readFile.ts +0 -14
- package/src/keymap/browser.ts +0 -335
- package/src/keymap/displayKey.ts +0 -17
- package/src/keymap/keymap.ts +0 -95
- package/src/layout/resolve.ts +0 -52
- package/src/layout/sidebarEmptyState.ts +0 -7
- package/src/layout/sidebarRow.ts +0 -69
- package/src/markdown/frontmatter.ts +0 -62
- package/src/serve/css.ts +0 -120
- package/src/serve/openBrowser.ts +0 -19
- package/src/serve/render.ts +0 -56
- package/src/serve/server.ts +0 -166
- package/src/theme/atom.ts +0 -23
- package/src/theme/colors.ts +0 -86
- package/src/theme/loader.ts +0 -110
- package/src/theme/registry.ts +0 -12
- package/src/theme/resolve.ts +0 -168
- package/src/theme/themes/aura.json +0 -58
- package/src/theme/themes/ayu.json +0 -69
- package/src/theme/themes/carbonfox.json +0 -201
- package/src/theme/themes/catppuccin-frappe.json +0 -186
- package/src/theme/themes/catppuccin-macchiato.json +0 -186
- package/src/theme/themes/catppuccin.json +0 -212
- package/src/theme/themes/cobalt2.json +0 -181
- package/src/theme/themes/cursor.json +0 -202
- package/src/theme/themes/dracula.json +0 -172
- package/src/theme/themes/everforest.json +0 -194
- package/src/theme/themes/flexoki.json +0 -190
- package/src/theme/themes/github.json +0 -186
- package/src/theme/themes/gruvbox.json +0 -195
- package/src/theme/themes/kanagawa.json +0 -180
- package/src/theme/themes/lucent-orng.json +0 -186
- package/src/theme/themes/material.json +0 -188
- package/src/theme/themes/matrix.json +0 -180
- package/src/theme/themes/mercury.json +0 -198
- package/src/theme/themes/monokai.json +0 -174
- package/src/theme/themes/nightowl.json +0 -174
- package/src/theme/themes/nord.json +0 -176
- package/src/theme/themes/one-dark.json +0 -184
- package/src/theme/themes/opencode.json +0 -198
- package/src/theme/themes/orng.json +0 -202
- package/src/theme/themes/osaka-jade.json +0 -193
- package/src/theme/themes/palenight.json +0 -175
- package/src/theme/themes/rosepine.json +0 -187
- package/src/theme/themes/solarized.json +0 -180
- package/src/theme/themes/synthwave84.json +0 -179
- package/src/theme/themes/tokyonight.json +0 -196
- package/src/theme/themes/vercel.json +0 -198
- package/src/theme/themes/vesper.json +0 -171
- package/src/theme/themes/zenburn.json +0 -176
- package/src/theme/types.ts +0 -115
- package/src/tips.ts +0 -88
- package/src/ui/middleTruncate.ts +0 -27
- package/src/update/cache.ts +0 -77
- package/src/update/check.ts +0 -165
- package/src/update/compare.ts +0 -41
- package/src/update/notice.ts +0 -29
- package/src/update/runtime.ts +0 -48
- package/src/update/useUpdateNotice.ts +0 -24
package/src/update/check.ts
DELETED
|
@@ -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
|
-
}
|
package/src/update/compare.ts
DELETED
|
@@ -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
|
-
}
|
package/src/update/notice.ts
DELETED
|
@@ -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")
|
package/src/update/runtime.ts
DELETED
|
@@ -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
|
-
}
|