@carlesandres/house 0.4.14 → 0.5.1

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 (87) hide show
  1. package/CHANGELOG.md +33 -2
  2. package/README.md +10 -8
  3. package/dist/bin.js +92 -0
  4. package/dist/index.js +10006 -0
  5. package/package.json +16 -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 -160
  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/rootLabel.ts +0 -18
  24. package/src/discovery/show.ts +0 -48
  25. package/src/discovery/walk.ts +0 -209
  26. package/src/index.tsx +0 -469
  27. package/src/io/clipboard.ts +0 -53
  28. package/src/io/editor.ts +0 -162
  29. package/src/io/readFile.ts +0 -14
  30. package/src/keymap/browser.ts +0 -335
  31. package/src/keymap/displayKey.ts +0 -17
  32. package/src/keymap/keymap.ts +0 -95
  33. package/src/layout/resolve.ts +0 -52
  34. package/src/layout/sidebarEmptyState.ts +0 -7
  35. package/src/layout/sidebarRow.ts +0 -69
  36. package/src/markdown/frontmatter.ts +0 -62
  37. package/src/serve/css.ts +0 -120
  38. package/src/serve/openBrowser.ts +0 -19
  39. package/src/serve/render.ts +0 -56
  40. package/src/serve/server.ts +0 -166
  41. package/src/theme/atom.ts +0 -23
  42. package/src/theme/colors.ts +0 -86
  43. package/src/theme/loader.ts +0 -110
  44. package/src/theme/registry.ts +0 -12
  45. package/src/theme/resolve.ts +0 -168
  46. package/src/theme/themes/aura.json +0 -58
  47. package/src/theme/themes/ayu.json +0 -69
  48. package/src/theme/themes/carbonfox.json +0 -201
  49. package/src/theme/themes/catppuccin-frappe.json +0 -186
  50. package/src/theme/themes/catppuccin-macchiato.json +0 -186
  51. package/src/theme/themes/catppuccin.json +0 -212
  52. package/src/theme/themes/cobalt2.json +0 -181
  53. package/src/theme/themes/cursor.json +0 -202
  54. package/src/theme/themes/dracula.json +0 -172
  55. package/src/theme/themes/everforest.json +0 -194
  56. package/src/theme/themes/flexoki.json +0 -190
  57. package/src/theme/themes/github.json +0 -186
  58. package/src/theme/themes/gruvbox.json +0 -195
  59. package/src/theme/themes/kanagawa.json +0 -180
  60. package/src/theme/themes/lucent-orng.json +0 -186
  61. package/src/theme/themes/material.json +0 -188
  62. package/src/theme/themes/matrix.json +0 -180
  63. package/src/theme/themes/mercury.json +0 -198
  64. package/src/theme/themes/monokai.json +0 -174
  65. package/src/theme/themes/nightowl.json +0 -174
  66. package/src/theme/themes/nord.json +0 -176
  67. package/src/theme/themes/one-dark.json +0 -184
  68. package/src/theme/themes/opencode.json +0 -198
  69. package/src/theme/themes/orng.json +0 -202
  70. package/src/theme/themes/osaka-jade.json +0 -193
  71. package/src/theme/themes/palenight.json +0 -175
  72. package/src/theme/themes/rosepine.json +0 -187
  73. package/src/theme/themes/solarized.json +0 -180
  74. package/src/theme/themes/synthwave84.json +0 -179
  75. package/src/theme/themes/tokyonight.json +0 -196
  76. package/src/theme/themes/vercel.json +0 -198
  77. package/src/theme/themes/vesper.json +0 -171
  78. package/src/theme/themes/zenburn.json +0 -176
  79. package/src/theme/types.ts +0 -115
  80. package/src/tips.ts +0 -88
  81. package/src/ui/middleTruncate.ts +0 -27
  82. package/src/update/cache.ts +0 -77
  83. package/src/update/check.ts +0 -165
  84. package/src/update/compare.ts +0 -41
  85. package/src/update/notice.ts +0 -29
  86. package/src/update/runtime.ts +0 -48
  87. 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
- }