claudeup 4.37.0 → 4.38.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/package.json +4 -4
- package/scripts/verify-community-registry.ts +272 -0
- package/src/__tests__/community-fetch.test.ts +545 -0
- package/src/__tests__/community-registry.test.ts +269 -0
- package/src/__tests__/community-staleness.test.ts +722 -0
- package/src/__tests__/open-file.test.ts +59 -0
- package/src/__tests__/style-wrap.test.ts +220 -0
- package/src/__tests__/styles-manager.test.ts +1124 -0
- package/src/__tests__/styles-origins.test.ts +416 -0
- package/src/__tests__/styles-screen-state.test.ts +460 -0
- package/src/__tests__/styles-status-line.test.ts +72 -0
- package/src/__tests__/styles-sync.test.ts +452 -0
- package/src/__tests__/tabbar-layout.test.ts +62 -0
- package/src/__tests__/terminology-filler.test.ts +214 -0
- package/src/data/community-styles.ts +521 -0
- package/src/main.tsx +15 -0
- package/src/services/catalog-cache-store.ts +101 -7
- package/src/services/community-fetcher.ts +90 -0
- package/src/services/community-styles.ts +1194 -0
- package/src/services/styles-manager.ts +1400 -0
- package/src/services/terminology-filler.ts +266 -0
- package/src/ui/App.tsx +15 -3
- package/src/ui/adapters/stylesAdapter.ts +403 -0
- package/src/ui/components/TabBar.tsx +43 -9
- package/src/ui/components/primitives/ActionHints.tsx +4 -1
- package/src/ui/components/primitives/ListCategoryRow.tsx +10 -1
- package/src/ui/registry.ts +6 -0
- package/src/ui/renderers/styleRenderers.tsx +809 -0
- package/src/ui/screens/StylesScreen.tsx +1089 -0
- package/src/ui/screens/index.ts +1 -0
- package/src/ui/state/reducer.ts +113 -1
- package/src/ui/state/types.ts +60 -2
- package/src/utils/open-file.ts +84 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { openInDefaultApp, openerFor } from "../utils/open-file.js";
|
|
3
|
+
|
|
4
|
+
describe("openerFor", () => {
|
|
5
|
+
test("uses the macOS opener on darwin", () => {
|
|
6
|
+
expect(openerFor("darwin")).toEqual({ command: "open", args: [] });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("uses xdg-open on linux", () => {
|
|
10
|
+
// claudeup ships a linux-x64 binary, and the pre-existing `open "${url}"`
|
|
11
|
+
// call elsewhere in the UI silently does nothing there.
|
|
12
|
+
expect(openerFor("linux")).toEqual({ command: "xdg-open", args: [] });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("passes an empty window title to cmd start on windows", () => {
|
|
16
|
+
// `start` treats its first quoted argument as the window title, so
|
|
17
|
+
// without the empty string a quoted path is consumed as one.
|
|
18
|
+
expect(openerFor("win32")).toEqual({
|
|
19
|
+
command: "cmd",
|
|
20
|
+
args: ["/c", "start", ""],
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("falls back to xdg-open on anything else", () => {
|
|
25
|
+
expect(openerFor("freebsd").command).toBe("xdg-open");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("openInDefaultApp", () => {
|
|
30
|
+
const missingOpener = {
|
|
31
|
+
command: "claudeup-test-opener-that-cannot-exist",
|
|
32
|
+
args: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
test("reports a missing opener rather than failing silently", async () => {
|
|
36
|
+
// An injected command that cannot exist, NOT a real platform's opener.
|
|
37
|
+
// The first version assumed `xdg-open` was absent — true on a Mac dev
|
|
38
|
+
// box, false on the Ubuntu CI runner, where it both failed the
|
|
39
|
+
// assertion and spawned a real opener on the build machine.
|
|
40
|
+
await expect(
|
|
41
|
+
openInDefaultApp("/tmp/whatever.md", "linux", missingOpener),
|
|
42
|
+
).rejects.toThrow(/not on PATH/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("settles promptly instead of hanging", async () => {
|
|
46
|
+
// The promise must settle when the child is spawned, not when the viewer
|
|
47
|
+
// is closed — waiting for exit would freeze the TUI for as long as the
|
|
48
|
+
// user's editor stayed open. Asserted on the error path so the suite
|
|
49
|
+
// never launches a real application on the developer's desktop.
|
|
50
|
+
const settled = await Promise.race([
|
|
51
|
+
openInDefaultApp("/tmp/whatever.md", "linux", missingOpener).then(
|
|
52
|
+
() => "resolved",
|
|
53
|
+
() => "rejected",
|
|
54
|
+
),
|
|
55
|
+
new Promise((resolve) => setTimeout(() => resolve("timeout"), 2000)),
|
|
56
|
+
]);
|
|
57
|
+
expect(settled).not.toBe("timeout");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
formatBody,
|
|
4
|
+
toBlocks,
|
|
5
|
+
upstreamLine,
|
|
6
|
+
wrapText,
|
|
7
|
+
} from "../ui/renderers/styleRenderers.js";
|
|
8
|
+
import { theme } from "../ui/theme.js";
|
|
9
|
+
|
|
10
|
+
describe("the Upstream line", () => {
|
|
11
|
+
const NOW = Date.parse("2026-08-18T12:00:00Z");
|
|
12
|
+
|
|
13
|
+
test("says up to date, with when it was checked", () => {
|
|
14
|
+
const line = upstreamLine(
|
|
15
|
+
{
|
|
16
|
+
state: "up-to-date",
|
|
17
|
+
detail: "up to date",
|
|
18
|
+
checkedAt: NOW - 3 * 3600_000,
|
|
19
|
+
},
|
|
20
|
+
NOW,
|
|
21
|
+
);
|
|
22
|
+
expect(line.text).toBe("up to date (checked 3h ago)");
|
|
23
|
+
expect(line.tone).toBe(theme.colors.success);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("an UNKNOWN state never renders as anything like currency", () => {
|
|
27
|
+
// The fail-closed clause, in the one place a user actually reads it. A
|
|
28
|
+
// rate-limited check, an offline box and a check that aged past its window
|
|
29
|
+
// all land here, and none of them is evidence of freshness.
|
|
30
|
+
for (const detail of [
|
|
31
|
+
"GitHub rate limit resets in 4m",
|
|
32
|
+
"no network — the styles you already fetched still work",
|
|
33
|
+
"not checked in the last 24h — press u",
|
|
34
|
+
]) {
|
|
35
|
+
const line = upstreamLine(
|
|
36
|
+
{ state: "unknown", detail, checkedAt: null },
|
|
37
|
+
NOW,
|
|
38
|
+
);
|
|
39
|
+
expect(line.text).toBe(`not checked — ${detail}`);
|
|
40
|
+
expect(line.text).not.toMatch(/up to date/);
|
|
41
|
+
expect(line.tone).not.toBe(theme.colors.success);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("a style that was never checked says so, and names the key", () => {
|
|
46
|
+
const line = upstreamLine(undefined, NOW);
|
|
47
|
+
expect(line.text).toBe("not checked — press u");
|
|
48
|
+
expect(line.tone).not.toBe(theme.colors.success);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("an available update carries its own wording through verbatim", () => {
|
|
52
|
+
const line = upstreamLine(
|
|
53
|
+
{
|
|
54
|
+
state: "update-available",
|
|
55
|
+
detail: "newer version available — 12 lines changed, press f to accept",
|
|
56
|
+
checkedAt: NOW,
|
|
57
|
+
changedLines: 12,
|
|
58
|
+
},
|
|
59
|
+
NOW,
|
|
60
|
+
);
|
|
61
|
+
expect(line.text).toMatch(/12 lines changed/);
|
|
62
|
+
expect(line.tone).toBe(theme.colors.warning);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("wrapText", () => {
|
|
67
|
+
test("leaves short lines exactly as they are", () => {
|
|
68
|
+
expect(wrapText("short line", 40)).toEqual(["short line"]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("never exceeds the column width", () => {
|
|
72
|
+
const text =
|
|
73
|
+
"Lead with the answer. The first sentence resolves the question; everything after it is support.";
|
|
74
|
+
for (const line of wrapText(text, 40)) {
|
|
75
|
+
expect(line.length).toBeLessThanOrEqual(40);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("emits a bullet once, not on every wrapped row", () => {
|
|
80
|
+
// The regression: extracting "- " as the indent and then re-prefixing it
|
|
81
|
+
// onto content that still began with "- " rendered "- - Lead with…".
|
|
82
|
+
const wrapped = wrapText(
|
|
83
|
+
"- Lead with the answer. The first sentence resolves the question; everything after it is support.",
|
|
84
|
+
40,
|
|
85
|
+
);
|
|
86
|
+
expect(wrapped.length).toBeGreaterThan(1);
|
|
87
|
+
expect(wrapped[0].startsWith("- ")).toBe(true);
|
|
88
|
+
expect(wrapped[0]).not.toContain("- - ");
|
|
89
|
+
for (const line of wrapped.slice(1)) {
|
|
90
|
+
expect(line.trimStart().startsWith("- ")).toBe(false);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("aligns continuation lines under the bullet text", () => {
|
|
95
|
+
const wrapped = wrapText(
|
|
96
|
+
"- Recommend, do not survey. When there are three viable approaches, name the one to take.",
|
|
97
|
+
40,
|
|
98
|
+
);
|
|
99
|
+
// "- " is two columns, so continuations are indented by two.
|
|
100
|
+
for (const line of wrapped.slice(1)) {
|
|
101
|
+
expect(line.startsWith(" ")).toBe(true);
|
|
102
|
+
expect(line.startsWith(" ")).toBe(false);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("fills the available width rather than breaking early", () => {
|
|
107
|
+
// The other half of the old bug: the per-line budget drifted with how much
|
|
108
|
+
// output had already been produced, so lines got progressively shorter.
|
|
109
|
+
const wrapped = wrapText(
|
|
110
|
+
"one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen",
|
|
111
|
+
40,
|
|
112
|
+
);
|
|
113
|
+
// Every line except the last should be within one word of the limit.
|
|
114
|
+
for (const line of wrapped.slice(0, -1)) {
|
|
115
|
+
expect(line.length).toBeGreaterThan(28);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("preserves blank lines between paragraphs", () => {
|
|
120
|
+
expect(wrapText("a\n\nb", 40)).toEqual(["a", "", "b"]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("does not drop a word that is longer than the width", () => {
|
|
124
|
+
const long = "x".repeat(60);
|
|
125
|
+
expect(wrapText(`- ${long}`, 40).join(" ")).toContain(long);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("keeps nested indentation", () => {
|
|
129
|
+
const wrapped = wrapText(
|
|
130
|
+
" deeply indented text that goes on long enough to need wrapping across lines",
|
|
131
|
+
40,
|
|
132
|
+
);
|
|
133
|
+
for (const line of wrapped) {
|
|
134
|
+
expect(line.startsWith(" ")).toBe(true);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("toBlocks", () => {
|
|
140
|
+
test("rejoins a bullet that the source hard-wrapped across lines", () => {
|
|
141
|
+
// Style presets ship wrapped at ~76 columns. Without rejoining, a 40-char
|
|
142
|
+
// continuation line already fits the panel and is emitted as its own
|
|
143
|
+
// ragged row — which is what the detail panel actually looked like.
|
|
144
|
+
expect(
|
|
145
|
+
toBlocks(
|
|
146
|
+
"- Lead with the answer. The first sentence resolves\n the question; everything after it is support.",
|
|
147
|
+
),
|
|
148
|
+
).toEqual([
|
|
149
|
+
"- Lead with the answer. The first sentence resolves the question; everything after it is support.",
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("a blank line separates blocks", () => {
|
|
154
|
+
expect(toBlocks("one\ntwo\n\nthree")).toEqual(["one two", "", "three"]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("each bullet is its own block", () => {
|
|
158
|
+
expect(toBlocks("- a\n cont\n- b")).toEqual(["- a cont", "- b"]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a heading never absorbs the line that follows it", () => {
|
|
162
|
+
expect(toBlocks("### Direct\nsome text")).toEqual([
|
|
163
|
+
"### Direct",
|
|
164
|
+
"some text",
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("table rows are passed through untouched", () => {
|
|
169
|
+
// Reflowing a table destroys its column alignment entirely.
|
|
170
|
+
expect(toBlocks("| a | b |\n| - | - |")).toEqual([
|
|
171
|
+
"| a | b |",
|
|
172
|
+
"| - | - |",
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("fenced code is preserved line by line", () => {
|
|
177
|
+
expect(toBlocks("```ts\nconst a = 1\nconst b = 2\n```")).toEqual([
|
|
178
|
+
"```ts",
|
|
179
|
+
"const a = 1",
|
|
180
|
+
"const b = 2",
|
|
181
|
+
"```",
|
|
182
|
+
]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("formatBody", () => {
|
|
187
|
+
test("produces even lines from hard-wrapped source", () => {
|
|
188
|
+
const body = [
|
|
189
|
+
"### Direct",
|
|
190
|
+
"",
|
|
191
|
+
"- Lead with the answer. The first sentence resolves the question;",
|
|
192
|
+
" everything after it is support. Never open with a restatement of",
|
|
193
|
+
" what was asked.",
|
|
194
|
+
].join("\n");
|
|
195
|
+
|
|
196
|
+
const lines = formatBody(body, 50);
|
|
197
|
+
for (const line of lines) expect(line.length).toBeLessThanOrEqual(50);
|
|
198
|
+
|
|
199
|
+
// The bullet's continuation rows should be full lines, not fragments
|
|
200
|
+
// inherited from the original 76-column source.
|
|
201
|
+
const continuations = lines.filter((l) => l.startsWith(" "));
|
|
202
|
+
expect(continuations.length).toBeGreaterThan(0);
|
|
203
|
+
for (const row of continuations.slice(0, -1)) {
|
|
204
|
+
expect(row.length).toBeGreaterThan(35);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("keeps the blank line between a heading and the list", () => {
|
|
209
|
+
expect(formatBody("### Direct\n\n- a", 50)).toEqual([
|
|
210
|
+
"### Direct",
|
|
211
|
+
"",
|
|
212
|
+
"- a",
|
|
213
|
+
]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("does not clip a table even when it overflows the width", () => {
|
|
217
|
+
const row = "| a very wide column | another very wide column |";
|
|
218
|
+
expect(formatBody(row, 20)).toEqual([row]);
|
|
219
|
+
});
|
|
220
|
+
});
|