@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,171 +0,0 @@
1
- {
2
- "defs": {
3
- "vesperBg": "#101010",
4
- "vesperFg": "#FFF",
5
- "vesperComment": "#8b8b8b",
6
- "vesperKeyword": "#A0A0A0",
7
- "vesperFunction": "#FFC799",
8
- "vesperString": "#99FFE4",
9
- "vesperNumber": "#FFC799",
10
- "vesperError": "#FF8080",
11
- "vesperWarning": "#FFC799",
12
- "vesperSuccess": "#99FFE4",
13
- "vesperMuted": "#A0A0A0"
14
- },
15
- "theme": {
16
- "primary": {
17
- "dark": "#FFC799",
18
- "light": "#FFC799"
19
- },
20
- "secondary": {
21
- "dark": "#99FFE4",
22
- "light": "#99FFE4"
23
- },
24
- "accent": {
25
- "dark": "#FFC799",
26
- "light": "#FFC799"
27
- },
28
- "error": {
29
- "dark": "vesperError",
30
- "light": "vesperError"
31
- },
32
- "warning": {
33
- "dark": "vesperWarning",
34
- "light": "vesperWarning"
35
- },
36
- "success": {
37
- "dark": "vesperSuccess",
38
- "light": "vesperSuccess"
39
- },
40
- "info": {
41
- "dark": "#FFC799",
42
- "light": "#FFC799"
43
- },
44
- "text": {
45
- "dark": "vesperFg",
46
- "light": "vesperBg"
47
- },
48
- "textMuted": {
49
- "dark": "vesperMuted",
50
- "light": "vesperMuted"
51
- },
52
- "background": {
53
- "dark": "vesperBg",
54
- "light": "#FFF"
55
- },
56
- "backgroundPanel": {
57
- "dark": "#151515",
58
- "light": "#F0F0F0"
59
- },
60
- "backgroundElement": {
61
- "dark": "#282828",
62
- "light": "#E0E0E0"
63
- },
64
- "border": {
65
- "dark": "#282828",
66
- "light": "#D0D0D0"
67
- },
68
- "borderActive": {
69
- "dark": "#FFC799",
70
- "light": "#FFC799"
71
- },
72
- "borderSubtle": {
73
- "dark": "#1C1C1C",
74
- "light": "#E8E8E8"
75
- },
76
- "markdownText": {
77
- "dark": "vesperFg",
78
- "light": "vesperBg"
79
- },
80
- "markdownHeading": {
81
- "dark": "#FFC799",
82
- "light": "#FFC799"
83
- },
84
- "markdownLink": {
85
- "dark": "#FFC799",
86
- "light": "#FFC799"
87
- },
88
- "markdownLinkText": {
89
- "dark": "vesperMuted",
90
- "light": "vesperMuted"
91
- },
92
- "markdownCode": {
93
- "dark": "vesperMuted",
94
- "light": "vesperMuted"
95
- },
96
- "markdownBlockQuote": {
97
- "dark": "vesperFg",
98
- "light": "vesperBg"
99
- },
100
- "markdownEmph": {
101
- "dark": "vesperFg",
102
- "light": "vesperBg"
103
- },
104
- "markdownStrong": {
105
- "dark": "vesperFg",
106
- "light": "vesperBg"
107
- },
108
- "markdownHorizontalRule": {
109
- "dark": "#65737E",
110
- "light": "#65737E"
111
- },
112
- "markdownListItem": {
113
- "dark": "vesperFg",
114
- "light": "vesperBg"
115
- },
116
- "markdownListEnumeration": {
117
- "dark": "vesperFg",
118
- "light": "vesperBg"
119
- },
120
- "markdownImage": {
121
- "dark": "#FFC799",
122
- "light": "#FFC799"
123
- },
124
- "markdownImageText": {
125
- "dark": "vesperMuted",
126
- "light": "vesperMuted"
127
- },
128
- "markdownCodeBlock": {
129
- "dark": "vesperFg",
130
- "light": "vesperBg"
131
- },
132
- "syntaxComment": {
133
- "dark": "vesperComment",
134
- "light": "vesperComment"
135
- },
136
- "syntaxKeyword": {
137
- "dark": "vesperKeyword",
138
- "light": "vesperKeyword"
139
- },
140
- "syntaxFunction": {
141
- "dark": "vesperFunction",
142
- "light": "vesperFunction"
143
- },
144
- "syntaxVariable": {
145
- "dark": "vesperFg",
146
- "light": "vesperBg"
147
- },
148
- "syntaxString": {
149
- "dark": "vesperString",
150
- "light": "vesperString"
151
- },
152
- "syntaxNumber": {
153
- "dark": "vesperNumber",
154
- "light": "vesperNumber"
155
- },
156
- "syntaxType": {
157
- "dark": "vesperFunction",
158
- "light": "vesperFunction"
159
- },
160
- "syntaxOperator": {
161
- "dark": "vesperKeyword",
162
- "light": "vesperKeyword"
163
- },
164
- "syntaxPunctuation": {
165
- "dark": "vesperFg",
166
- "light": "vesperBg"
167
- }
168
- },
169
- "name": "Vesper",
170
- "$schema": "../../../schema/house-theme.schema.json"
171
- }
@@ -1,176 +0,0 @@
1
- {
2
- "defs": {
3
- "bg": "#3f3f3f",
4
- "bgAlt": "#4f4f4f",
5
- "bgPanel": "#5f5f5f",
6
- "fg": "#dcdccc",
7
- "fgMuted": "#9f9f9f",
8
- "red": "#cc9393",
9
- "redBright": "#dca3a3",
10
- "green": "#7f9f7f",
11
- "greenBright": "#8fb28f",
12
- "yellow": "#f0dfaf",
13
- "yellowDim": "#e0cf9f",
14
- "blue": "#8cd0d3",
15
- "blueDim": "#7cb8bb",
16
- "magenta": "#dc8cc3",
17
- "cyan": "#93e0e3",
18
- "orange": "#dfaf8f"
19
- },
20
- "theme": {
21
- "primary": {
22
- "dark": "blue",
23
- "light": "#5f7f8f"
24
- },
25
- "secondary": {
26
- "dark": "magenta",
27
- "light": "#8f5f8f"
28
- },
29
- "accent": {
30
- "dark": "cyan",
31
- "light": "#5f8f8f"
32
- },
33
- "error": {
34
- "dark": "red",
35
- "light": "#8f5f5f"
36
- },
37
- "warning": {
38
- "dark": "yellow",
39
- "light": "#8f8f5f"
40
- },
41
- "success": {
42
- "dark": "green",
43
- "light": "#5f8f5f"
44
- },
45
- "info": {
46
- "dark": "orange",
47
- "light": "#8f7f5f"
48
- },
49
- "text": {
50
- "dark": "fg",
51
- "light": "#3f3f3f"
52
- },
53
- "textMuted": {
54
- "dark": "fgMuted",
55
- "light": "#6f6f6f"
56
- },
57
- "background": {
58
- "dark": "bg",
59
- "light": "#ffffef"
60
- },
61
- "backgroundPanel": {
62
- "dark": "bgAlt",
63
- "light": "#f5f5e5"
64
- },
65
- "backgroundElement": {
66
- "dark": "bgPanel",
67
- "light": "#ebebdb"
68
- },
69
- "border": {
70
- "dark": "#5f5f5f",
71
- "light": "#d0d0c0"
72
- },
73
- "borderActive": {
74
- "dark": "blue",
75
- "light": "#5f7f8f"
76
- },
77
- "borderSubtle": {
78
- "dark": "#4f4f4f",
79
- "light": "#e0e0d0"
80
- },
81
- "markdownText": {
82
- "dark": "fg",
83
- "light": "#3f3f3f"
84
- },
85
- "markdownHeading": {
86
- "dark": "yellow",
87
- "light": "#8f8f5f"
88
- },
89
- "markdownLink": {
90
- "dark": "blue",
91
- "light": "#5f7f8f"
92
- },
93
- "markdownLinkText": {
94
- "dark": "cyan",
95
- "light": "#5f8f8f"
96
- },
97
- "markdownCode": {
98
- "dark": "green",
99
- "light": "#5f8f5f"
100
- },
101
- "markdownBlockQuote": {
102
- "dark": "fgMuted",
103
- "light": "#6f6f6f"
104
- },
105
- "markdownEmph": {
106
- "dark": "yellowDim",
107
- "light": "#8f8f5f"
108
- },
109
- "markdownStrong": {
110
- "dark": "orange",
111
- "light": "#8f7f5f"
112
- },
113
- "markdownHorizontalRule": {
114
- "dark": "fgMuted",
115
- "light": "#6f6f6f"
116
- },
117
- "markdownListItem": {
118
- "dark": "blue",
119
- "light": "#5f7f8f"
120
- },
121
- "markdownListEnumeration": {
122
- "dark": "cyan",
123
- "light": "#5f8f8f"
124
- },
125
- "markdownImage": {
126
- "dark": "blue",
127
- "light": "#5f7f8f"
128
- },
129
- "markdownImageText": {
130
- "dark": "cyan",
131
- "light": "#5f8f8f"
132
- },
133
- "markdownCodeBlock": {
134
- "dark": "fg",
135
- "light": "#3f3f3f"
136
- },
137
- "syntaxComment": {
138
- "dark": "#7f9f7f",
139
- "light": "#5f7f5f"
140
- },
141
- "syntaxKeyword": {
142
- "dark": "yellow",
143
- "light": "#8f8f5f"
144
- },
145
- "syntaxFunction": {
146
- "dark": "blue",
147
- "light": "#5f7f8f"
148
- },
149
- "syntaxVariable": {
150
- "dark": "fg",
151
- "light": "#3f3f3f"
152
- },
153
- "syntaxString": {
154
- "dark": "red",
155
- "light": "#8f5f5f"
156
- },
157
- "syntaxNumber": {
158
- "dark": "greenBright",
159
- "light": "#5f8f5f"
160
- },
161
- "syntaxType": {
162
- "dark": "cyan",
163
- "light": "#5f8f8f"
164
- },
165
- "syntaxOperator": {
166
- "dark": "yellow",
167
- "light": "#8f8f5f"
168
- },
169
- "syntaxPunctuation": {
170
- "dark": "fg",
171
- "light": "#3f3f3f"
172
- }
173
- },
174
- "name": "Zenburn",
175
- "$schema": "../../../schema/house-theme.schema.json"
176
- }
@@ -1,115 +0,0 @@
1
- import type { StyleDefinitionInput } from "@opentui/core"
2
-
3
- export type HexColor = `#${string}`
4
- export type RefName = string
5
-
6
- /**
7
- * A token's value. Either a literal hex (`"#abcdef"`), a reference to an
8
- * entry in the theme's `defs` block (`"nord8"`), or a `{dark, light}`
9
- * variant where each side is itself a hex or a defs ref.
10
- */
11
- export type ColorValue = HexColor | RefName | { readonly dark: string; readonly light: string }
12
-
13
- /** Light or dark mode selector. Each theme JSON pairs both. */
14
- export type Tone = "dark" | "light"
15
-
16
- /**
17
- * The complete house token surface. Adapted from opencode's TUI theme
18
- * (see reference/opencode/.../tui/context/theme/) by dropping the `diff*`
19
- * cluster, `backgroundMenu`, and `thinkingOpacity` (none of which are
20
- * reachable in a markdown reader).
21
- */
22
- export interface ThemeTokens {
23
- // UI semantics
24
- readonly primary: ColorValue
25
- readonly secondary: ColorValue
26
- readonly accent: ColorValue
27
- readonly error: ColorValue
28
- readonly warning: ColorValue
29
- readonly success: ColorValue
30
- readonly info: ColorValue
31
- readonly text: ColorValue
32
- readonly textMuted: ColorValue
33
- readonly selectedListItemText?: ColorValue
34
- readonly background: ColorValue
35
- readonly backgroundPanel: ColorValue
36
- readonly backgroundElement: ColorValue
37
- readonly border: ColorValue
38
- readonly borderActive: ColorValue
39
- readonly borderSubtle: ColorValue
40
- // Markdown rendering
41
- readonly markdownText: ColorValue
42
- readonly markdownHeading: ColorValue
43
- readonly markdownLink: ColorValue
44
- readonly markdownLinkText: ColorValue
45
- readonly markdownCode: ColorValue
46
- readonly markdownBlockQuote: ColorValue
47
- readonly markdownEmph: ColorValue
48
- readonly markdownStrong: ColorValue
49
- readonly markdownHorizontalRule: ColorValue
50
- readonly markdownListItem: ColorValue
51
- readonly markdownListEnumeration: ColorValue
52
- readonly markdownImage: ColorValue
53
- readonly markdownImageText: ColorValue
54
- readonly markdownCodeBlock: ColorValue
55
- // Fenced-code syntax
56
- readonly syntaxComment: ColorValue
57
- readonly syntaxKeyword: ColorValue
58
- readonly syntaxFunction: ColorValue
59
- readonly syntaxVariable: ColorValue
60
- readonly syntaxString: ColorValue
61
- readonly syntaxNumber: ColorValue
62
- readonly syntaxType: ColorValue
63
- readonly syntaxOperator: ColorValue
64
- readonly syntaxPunctuation: ColorValue
65
- }
66
-
67
- /** Raw on-disk shape for a theme JSON file. */
68
- export interface ThemeJson {
69
- readonly $schema?: string
70
- readonly name?: string
71
- readonly defs?: Record<string, string>
72
- readonly theme: Partial<ThemeTokens>
73
- }
74
-
75
- /** Token names that consumers may reference. */
76
- export type TokenName = keyof ThemeTokens
77
-
78
- /**
79
- * Flat record after `resolveTheme`: every token has a concrete `#rrggbb`
80
- * value with the requested tone applied and defs refs resolved.
81
- */
82
- export type ResolvedTheme = Readonly<Record<TokenName, HexColor>>
83
-
84
- /**
85
- * The flat color object consumed by Browser/HelpOverlay/index. Uses the
86
- * OpenCode-aligned UI token names directly (`background`, `backgroundPanel`,
87
- * `backgroundElement`, `borderSubtle`, `primary`, `secondary`, …). Values
88
- * come from `colors.ts`'s adapter, which maps `ResolvedTheme` → this shape.
89
- */
90
- export interface ColorPalette {
91
- readonly background: string
92
- readonly backgroundPanel: string
93
- readonly backgroundElement: string
94
- readonly text: string
95
- readonly textMuted: string
96
- readonly border: string
97
- readonly borderActive: string
98
- readonly borderSubtle: string
99
- readonly selectedListItemText: string
100
- readonly primary: string
101
- readonly secondary: string
102
- readonly accent: string
103
- readonly error: string
104
- readonly warning: string
105
- readonly success: string
106
- readonly info: string
107
- readonly syntax: Record<string, StyleDefinitionInput>
108
- }
109
-
110
- /** Registry entry for a theme; what the loader returns. */
111
- export interface ThemeDefinition {
112
- readonly id: string
113
- readonly name: string
114
- readonly source: ThemeJson
115
- }
package/src/tips.ts DELETED
@@ -1,88 +0,0 @@
1
- import type { BrowserCtx } from "./keymap/browser.ts"
2
- import type { KeyBinding } from "./keymap/keymap.ts"
3
- import { displayKey } from "./keymap/displayKey.ts"
4
-
5
- export interface TipLine {
6
- readonly id: string
7
- readonly text: string
8
- }
9
-
10
- interface TipDefinition {
11
- readonly id: string
12
- readonly bindingId?: string
13
- readonly render: (parts: { readonly key: string | null }) => string
14
- readonly when?: (ctx: BrowserCtx) => boolean
15
- }
16
-
17
- const tipDefinitions: readonly TipDefinition[] = [
18
- {
19
- id: "filter.start",
20
- bindingId: "filter.open",
21
- render: ({ key }) => `Press ${key ?? "/"} to start filtering files by path.`,
22
- when: (ctx) => ctx.filterQuery.length === 0 && !ctx.filterOpen,
23
- },
24
- {
25
- id: "filter.resume",
26
- bindingId: "filter.open",
27
- render: ({ key }) => `Press ${key ?? "/"} to reopen the current filter and keep refining it.`,
28
- when: (ctx) => ctx.filterQuery.length > 0 && !ctx.filterOpen,
29
- },
30
- {
31
- id: "filter.commit",
32
- bindingId: "filter.open",
33
- render: () => "Press Enter in the filter to open the selected match in the reader.",
34
- when: (ctx) => ctx.filterQuery.length === 0,
35
- },
36
- {
37
- id: "filter.clear",
38
- bindingId: "filter.clearOrOpen",
39
- render: ({ key }) => `Press ${key ?? "ctrl+\\"} to clear the current filter and start over.`,
40
- when: (ctx) => ctx.filterQuery.length > 0,
41
- },
42
- {
43
- id: "filtered-navigation",
44
- render: () => "Use [ and ] to move through files while the current filter stays applied.",
45
- when: (ctx) => ctx.filterQuery.length > 0,
46
- },
47
- {
48
- id: "focus.toggle",
49
- bindingId: "focus.toggle",
50
- render: ({ key }) => `Press ${key ?? "tab"} to switch between the sidebar and reader.`,
51
- },
52
- {
53
- id: "sidebar.toggle",
54
- bindingId: "sidebar.toggle",
55
- render: ({ key }) =>
56
- `Press ${key ?? "s"} to hide or show the sidebar without losing your place.`,
57
- },
58
- ]
59
-
60
- const firstKeyByBindingId = <C>(bindings: readonly KeyBinding<C>[]): ReadonlyMap<string, string> =>
61
- new Map(
62
- bindings.flatMap((binding) => {
63
- const firstKey = binding.keys[0]
64
- return firstKey ? [[binding.id, displayKey(firstKey)] as const] : []
65
- }),
66
- )
67
-
68
- export const buildReaderEmptyStateTips = (
69
- bindings: readonly KeyBinding<BrowserCtx>[],
70
- ctx: BrowserCtx,
71
- ): readonly TipLine[] => {
72
- const keyByBindingId = firstKeyByBindingId(bindings)
73
-
74
- return tipDefinitions
75
- .filter((tip) => !tip.when || tip.when(ctx))
76
- .map((tip) => ({
77
- id: tip.id,
78
- text: `Tip: ${tip.render({ key: tip.bindingId ? (keyByBindingId.get(tip.bindingId) ?? null) : null })}`,
79
- }))
80
- }
81
-
82
- export const pickTipByRotation = (
83
- tips: readonly TipLine[],
84
- rotationIndex: number,
85
- ): TipLine | null => {
86
- if (tips.length === 0) return null
87
- return tips[((rotationIndex % tips.length) + tips.length) % tips.length] ?? null
88
- }
@@ -1,27 +0,0 @@
1
- /**
2
- * Middle truncation for technical strings.
3
- *
4
- * Keeps the start and end of a string visible while compressing the middle
5
- * to a single ellipsis glyph (`…`). Use for paths, IDs, branch names, and
6
- * other values whose head and tail both matter.
7
- */
8
-
9
- export interface MiddleTruncateOptions {
10
- readonly ellipsis?: string
11
- }
12
-
13
- export const middleTruncate = (
14
- value: string,
15
- width: number,
16
- options: MiddleTruncateOptions = {},
17
- ): string => {
18
- const ellipsis = options.ellipsis ?? "…"
19
- if (width <= 0) return ""
20
- if (value.length <= width) return value
21
- if (width <= ellipsis.length) return value.slice(0, width)
22
-
23
- const available = width - ellipsis.length
24
- const left = Math.ceil(available / 2)
25
- const right = Math.floor(available / 2)
26
- return value.slice(0, left) + ellipsis + value.slice(value.length - right)
27
- }
@@ -1,77 +0,0 @@
1
- /**
2
- * Update-check cache. Stores the last npm-registry probe result on disk so
3
- * we hit the registry at most once per TTL window.
4
- *
5
- * Layout: `$XDG_CACHE_HOME/house/update-check.json` (fallback
6
- * `~/.cache/house/update-check.json`). All IO failures are non-fatal — a
7
- * missing or unparseable cache simply forces a fresh probe.
8
- *
9
- * Schema is intentionally minimal. `tarballOk` distinguishes "we saw the
10
- * version and confirmed the tarball is downloadable" from "we saw the
11
- * version but the CDN HEAD failed" — only the former gates the notice. A
12
- * `false` entry forces a retry on the next launch rather than waiting out
13
- * the TTL.
14
- */
15
-
16
- import { dirname, join } from "node:path"
17
- import { homedir } from "node:os"
18
- import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"
19
-
20
- export interface UpdateCacheRecord {
21
- readonly checkedAt: number
22
- readonly latestVersion: string
23
- readonly tarballOk: boolean
24
- }
25
-
26
- export const cacheDir = (): string => {
27
- const xdg = process.env.XDG_CACHE_HOME
28
- if (xdg && xdg.length > 0) return join(xdg, "house")
29
- return join(homedir(), ".cache", "house")
30
- }
31
-
32
- export const cachePath = (): string => join(cacheDir(), "update-check.json")
33
-
34
- export const readCache = async (path = cachePath()): Promise<UpdateCacheRecord | null> => {
35
- try {
36
- const raw = await readFile(path, "utf8")
37
- const parsed = JSON.parse(raw) as unknown
38
- if (typeof parsed !== "object" || parsed === null) return null
39
- const r = parsed as Record<string, unknown>
40
- if (
41
- typeof r.checkedAt !== "number" ||
42
- typeof r.latestVersion !== "string" ||
43
- typeof r.tarballOk !== "boolean"
44
- ) {
45
- return null
46
- }
47
- return {
48
- checkedAt: r.checkedAt,
49
- latestVersion: r.latestVersion,
50
- tarballOk: r.tarballOk,
51
- }
52
- } catch {
53
- return null
54
- }
55
- }
56
-
57
- export const writeCache = async (record: UpdateCacheRecord, path = cachePath()): Promise<void> => {
58
- // Atomic write: writeFile to a sibling tmp path, then rename. Rename is
59
- // atomic on POSIX, so a process.exit() racing the writer either leaves
60
- // the prior file intact or installs the new one fully — never a partial
61
- // JSON blob that readCache would have to ignore.
62
- const tmp = `${path}.${process.pid}.${Date.now()}.tmp`
63
- try {
64
- await mkdir(dirname(path), { recursive: true })
65
- await writeFile(tmp, JSON.stringify(record), "utf8")
66
- await rename(tmp, path)
67
- } catch {
68
- // Cache writes are best-effort. A read-only HOME or full disk should
69
- // not break the app; we'll just re-probe on the next launch. Clean up
70
- // the tmp file if writeFile partially succeeded but rename did not.
71
- try {
72
- await unlink(tmp)
73
- } catch {
74
- // best-effort
75
- }
76
- }
77
- }