claudeup 4.36.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.
Files changed (47) hide show
  1. package/package.json +4 -4
  2. package/scripts/verify-community-registry.ts +272 -0
  3. package/src/__tests__/catalog-cache-store.test.ts +271 -0
  4. package/src/__tests__/catalog-notice.test.ts +155 -0
  5. package/src/__tests__/community-fetch.test.ts +545 -0
  6. package/src/__tests__/community-registry.test.ts +269 -0
  7. package/src/__tests__/community-staleness.test.ts +722 -0
  8. package/src/__tests__/github-budget.test.ts +200 -0
  9. package/src/__tests__/open-file.test.ts +59 -0
  10. package/src/__tests__/plugin-manager-fallback.test.ts +200 -8
  11. package/src/__tests__/style-wrap.test.ts +220 -0
  12. package/src/__tests__/styles-manager.test.ts +1124 -0
  13. package/src/__tests__/styles-origins.test.ts +416 -0
  14. package/src/__tests__/styles-screen-state.test.ts +460 -0
  15. package/src/__tests__/styles-status-line.test.ts +72 -0
  16. package/src/__tests__/styles-sync.test.ts +452 -0
  17. package/src/__tests__/tabbar-layout.test.ts +62 -0
  18. package/src/__tests__/terminology-filler.test.ts +214 -0
  19. package/src/data/community-styles.ts +521 -0
  20. package/src/main.tsx +15 -0
  21. package/src/services/catalog-cache-store.ts +312 -0
  22. package/src/services/community-fetcher.ts +90 -0
  23. package/src/services/community-styles.ts +1194 -0
  24. package/src/services/github-budget.ts +274 -0
  25. package/src/services/marketplace-catalog-git.ts +170 -0
  26. package/src/services/marketplace-catalog.ts +95 -0
  27. package/src/services/marketplace-fetcher.ts +310 -87
  28. package/src/services/plugin-manager.ts +103 -92
  29. package/src/services/styles-manager.ts +1400 -0
  30. package/src/services/terminology-filler.ts +266 -0
  31. package/src/ui/App.tsx +15 -3
  32. package/src/ui/adapters/catalogNotice.ts +122 -0
  33. package/src/ui/adapters/stylesAdapter.ts +403 -0
  34. package/src/ui/components/TabBar.tsx +43 -9
  35. package/src/ui/components/layout/ScreenLayout.tsx +19 -2
  36. package/src/ui/components/primitives/ActionHints.tsx +4 -1
  37. package/src/ui/components/primitives/ListCategoryRow.tsx +10 -1
  38. package/src/ui/registry.ts +6 -0
  39. package/src/ui/renderers/pluginRenderers.tsx +39 -1
  40. package/src/ui/renderers/styleRenderers.tsx +809 -0
  41. package/src/ui/screens/PluginsScreen.tsx +138 -29
  42. package/src/ui/screens/StylesScreen.tsx +1089 -0
  43. package/src/ui/screens/index.ts +1 -0
  44. package/src/ui/state/reducer.ts +124 -3
  45. package/src/ui/state/types.ts +76 -3
  46. package/src/utils/config-dir.ts +47 -0
  47. package/src/utils/open-file.ts +84 -0
@@ -0,0 +1,521 @@
1
+ /**
2
+ * community-styles.ts — the curated registry of third-party output styles.
3
+ *
4
+ * COORDINATES ONLY. This file ships `(repo, path, ref, licence, attribution)`
5
+ * and never a byte of anyone else's style text. The text is fetched from the
6
+ * source repository on an explicit user action and cached outside
7
+ * `~/.claude/output-styles/`; nothing here redistributes it.
8
+ *
9
+ * ## What earns an entry
10
+ *
11
+ * A repo qualifies **iff** walking its git tree finds at least one `*.md` under
12
+ * a styles directory. `SKILL.md` count is irrelevant. That rule is not
13
+ * pedantry: the two highest-starred names in this space ship ZERO style files
14
+ * (see `DENIED_REPOS`), so a registry built from a popularity list would 404 on
15
+ * its most prominent entries.
16
+ *
17
+ * Paths are VERIFIED, never inferred. `scripts/verify-community-registry.ts`
18
+ * lists the real `.md` blobs in a repo (`--discover`) and re-fetches every
19
+ * committed entry (`--check`). A wrong path is a 404 the user sees, and the
20
+ * error message blames us for it, correctly.
21
+ *
22
+ * ## What this file must never become
23
+ *
24
+ * An arbitrary-URL fetcher. The set of fetchable things being a constant in a
25
+ * reviewed commit is the single strongest control on a feature whose output
26
+ * becomes system-prompt text.
27
+ */
28
+
29
+ import { isValidGitHubRepo } from "../utils/string-utils.js";
30
+
31
+ /** SPDX id, or null when the repo grants no redistribution licence. */
32
+ export type StyleLicence = "MIT" | "AGPL-3.0" | "Apache-2.0" | null;
33
+
34
+ /** One upstream repository. Shared by every style it ships. */
35
+ export interface CommunityStyleSource {
36
+ /** Registry slug, and the first half of every id from this repo. Never changes. */
37
+ id: string;
38
+ /** `owner/name`, as GitHub spells it. */
39
+ repo: string;
40
+ /**
41
+ * Directory the styles live in. NOT a convention — four of these use
42
+ * `output-styles/` and one uses `.claude/output-styles/`, so nothing here may
43
+ * glob a fixed name.
44
+ */
45
+ dir: string;
46
+ /** Git ref to fetch content from. `HEAD` means "whatever is current". */
47
+ ref: string;
48
+ licence: StyleLicence;
49
+ licenceUrl: string | null;
50
+ /** Display attribution. */
51
+ author: string;
52
+ homepage: string;
53
+ /** Curation-time snapshot of the last push, for context only. */
54
+ pushedAt: string;
55
+ /**
56
+ * Retired sources are no longer offered but still resolve, so a committed
57
+ * `.claude/style.json` naming one keeps working. Entries are never deleted.
58
+ */
59
+ retired?: true;
60
+ }
61
+
62
+ export interface CommunityStyle {
63
+ /** `<sourceId>--<slug>`. The stable coordinate id — see the note below. */
64
+ id: string;
65
+ sourceId: string;
66
+ /** Path within the repo, INCLUDING the source's `dir`. Verified, never inferred. */
67
+ path: string;
68
+ /** What the list shows: upstream's own `name`, recorded at curation time. */
69
+ displayName: string;
70
+ /** One line, ours, kept short — upstream descriptions vary wildly in length. */
71
+ summary: string;
72
+ /** Retired styles still resolve for an old declaration; see `retired` above. */
73
+ retired?: true;
74
+ }
75
+
76
+ /**
77
+ * Why ids are coordinates rather than the file's own `frontmatter.name`.
78
+ *
79
+ * Three reasons, the third decisive:
80
+ * 1. It survives upstream renaming the style.
81
+ * 2. Upstream names are arbitrary human strings ("Zen Master", "ADHD") and a
82
+ * comma in one corrupts the comma-separated `style-imports` round-trip.
83
+ * 3. A teammate who clones a repo whose `.claude/style.json` names
84
+ * `community:attention-span--spartan` has fetched nothing. To offer "press f
85
+ * to fetch this" rather than a bare "missing", the id must resolve against
86
+ * this registry BEFORE any file exists locally. An id derived from the
87
+ * file's own frontmatter is unknowable until after the fetch.
88
+ */
89
+ export const COMMUNITY_ID_SEPARATOR = "--";
90
+
91
+ // ─── Sources ─────────────────────────────────────────────────────────────────
92
+
93
+ export const COMMUNITY_SOURCES: CommunityStyleSource[] = [
94
+ {
95
+ id: "attention-span",
96
+ repo: "alexgreensh/attention-span",
97
+ dir: "output-styles",
98
+ ref: "HEAD",
99
+ licence: "AGPL-3.0",
100
+ licenceUrl:
101
+ "https://github.com/alexgreensh/attention-span/blob/HEAD/LICENSE",
102
+ author: "alexgreensh",
103
+ homepage: "https://github.com/alexgreensh/attention-span",
104
+ pushedAt: "2026-08-15",
105
+ },
106
+ {
107
+ id: "claude-mods",
108
+ repo: "0xDarkMatter/claude-mods",
109
+ dir: "output-styles",
110
+ ref: "HEAD",
111
+ licence: "MIT",
112
+ licenceUrl: "https://github.com/0xDarkMatter/claude-mods/blob/HEAD/LICENSE",
113
+ author: "0xDarkMatter",
114
+ homepage: "https://github.com/0xDarkMatter/claude-mods",
115
+ pushedAt: "2026-08-15",
116
+ },
117
+ {
118
+ id: "lej-output-fixer",
119
+ repo: "justfinethanku/LEJ-output-fixer",
120
+ dir: "output-styles",
121
+ ref: "HEAD",
122
+ licence: "MIT",
123
+ licenceUrl:
124
+ "https://github.com/justfinethanku/LEJ-output-fixer/blob/HEAD/LICENSE",
125
+ author: "justfinethanku",
126
+ homepage: "https://github.com/justfinethanku/LEJ-output-fixer",
127
+ pushedAt: "2026-08-09",
128
+ },
129
+ {
130
+ // The only source whose styles are NOT in `output-styles/`. Shipping it is
131
+ // what keeps the per-entry `path` exercised in production rather than
132
+ // merely designed for.
133
+ id: "hesreallyhim",
134
+ repo: "hesreallyhim/awesome-claude-code-output-styles-that-i-really-like",
135
+ dir: ".claude/output-styles",
136
+ ref: "HEAD",
137
+ licence: "MIT",
138
+ licenceUrl:
139
+ "https://github.com/hesreallyhim/awesome-claude-code-output-styles-that-i-really-like/blob/HEAD/LICENSE",
140
+ author: "hesreallyhim",
141
+ homepage:
142
+ "https://github.com/hesreallyhim/awesome-claude-code-output-styles-that-i-really-like",
143
+ pushedAt: "2025-11-21",
144
+ },
145
+ {
146
+ id: "nattergabriel",
147
+ repo: "nattergabriel/claude-code-output-styles",
148
+ dir: "output-styles",
149
+ ref: "HEAD",
150
+ licence: "MIT",
151
+ licenceUrl:
152
+ "https://github.com/nattergabriel/claude-code-output-styles/blob/HEAD/LICENSE",
153
+ author: "nattergabriel",
154
+ homepage: "https://github.com/nattergabriel/claude-code-output-styles",
155
+ pushedAt: "2026-04-03",
156
+ },
157
+ {
158
+ id: "simple-english",
159
+ repo: "AminBlg/SimpleEnglish",
160
+ dir: "output-styles",
161
+ ref: "HEAD",
162
+ licence: "MIT",
163
+ licenceUrl: "https://github.com/AminBlg/SimpleEnglish/blob/HEAD/LICENSE",
164
+ author: "AminBlg",
165
+ homepage: "https://github.com/AminBlg/SimpleEnglish",
166
+ pushedAt: "2026-08-17",
167
+ },
168
+ ];
169
+
170
+ // ─── Styles ──────────────────────────────────────────────────────────────────
171
+
172
+ /**
173
+ * 22 entries across the 5 sources above.
174
+ *
175
+ * Every `path` and every `displayName` here was produced by
176
+ * `scripts/verify-community-registry.ts --discover`, which lists the repo's
177
+ * real `.md` blobs and reads each file's own `name`. None was typed from a
178
+ * README. `--check` was clean before this was committed.
179
+ *
180
+ * `displayName` is upstream's `name` verbatim, warts included — `claude-mods`
181
+ * writes lowercase slugs, `hesreallyhim` writes title case, and
182
+ * `existentialist-poet.md` calls itself "Existential Poet". Normalising any of
183
+ * that would put a name in the list that appears nowhere upstream.
184
+ *
185
+ * `summary` is OURS: one line, under 100 characters, in our register. Upstream
186
+ * descriptions run from 40 to 200+ characters and several are marketing.
187
+ *
188
+ * Not everything importable is listed. `claude-mods` and `nattergabriel` ship
189
+ * 13 each and 5 of each are here — the rest are unreviewed, and adding one is a
190
+ * one-line diff, so deferring costs nothing. Entries are never DELETED, only
191
+ * marked `retired`: a deleted id breaks a committed `.claude/style.json` on
192
+ * every teammate's machine.
193
+ */
194
+ export const COMMUNITY_STYLES: CommunityStyle[] = [
195
+ // ── attention-span ───────────────────────────────────────────────────────
196
+ {
197
+ id: "attention-span--attention-kind",
198
+ sourceId: "attention-span",
199
+ path: "output-styles/attention-kind.md",
200
+ displayName: "Attention-kind",
201
+ summary:
202
+ "ADHD-friendly. Plain English, answers front-loaded, expands only where it matters.",
203
+ },
204
+ {
205
+ id: "attention-span--rundown",
206
+ sourceId: "attention-span",
207
+ path: "output-styles/rundown.md",
208
+ displayName: "Rundown",
209
+ summary:
210
+ "Briefing format. TL;DR first, then state as checkboxes and tagged choices.",
211
+ },
212
+ {
213
+ id: "attention-span--spartan",
214
+ sourceId: "attention-span",
215
+ path: "output-styles/spartan.md",
216
+ displayName: "Spartan",
217
+ summary: "Blunt and answer-first. Arrow points, no warmth, no filler.",
218
+ },
219
+
220
+ // ── claude-mods (5 of 13) ────────────────────────────────────────────────
221
+ {
222
+ id: "claude-mods--executive",
223
+ sourceId: "claude-mods",
224
+ path: "output-styles/executive.md",
225
+ displayName: "executive",
226
+ summary:
227
+ "Stakeholder brief. Decisions, impact and timelines, with the engineering left out.",
228
+ },
229
+ {
230
+ id: "claude-mods--mentor",
231
+ sourceId: "claude-mods",
232
+ path: "output-styles/mentor.md",
233
+ displayName: "mentor",
234
+ summary:
235
+ "Teaching voice. Explains why before what, and builds understanding as it goes.",
236
+ },
237
+ {
238
+ id: "claude-mods--noir",
239
+ sourceId: "claude-mods",
240
+ path: "output-styles/noir.md",
241
+ displayName: "noir",
242
+ summary:
243
+ "Hard-boiled detective narrating your codebase. Pure persona, no workflow changes.",
244
+ },
245
+ {
246
+ id: "claude-mods--pair",
247
+ sourceId: "claude-mods",
248
+ path: "output-styles/pair.md",
249
+ displayName: "pair",
250
+ summary:
251
+ "Pairing voice. Thinks out loud and shares the driver's seat rather than dictating.",
252
+ },
253
+ {
254
+ id: "claude-mods--roast",
255
+ sourceId: "claude-mods",
256
+ path: "output-styles/roast.md",
257
+ displayName: "roast",
258
+ summary:
259
+ "Adversarial review. Brutally honest about your code, then helps you fix it.",
260
+ },
261
+
262
+ // ── lej-output-fixer (3 of 3) ────────────────────────────────────────────
263
+ {
264
+ id: "lej-output-fixer--bottom-line-first",
265
+ sourceId: "lej-output-fixer",
266
+ path: "output-styles/bottom-line-first.md",
267
+ displayName: "Bottom Line First",
268
+ summary:
269
+ "Verdict up top. Details appear only when they would change a decision.",
270
+ },
271
+ {
272
+ id: "lej-output-fixer--scannable",
273
+ sourceId: "lej-output-fixer",
274
+ path: "output-styles/scannable.md",
275
+ displayName: "Scannable",
276
+ summary:
277
+ "Short paragraphs, plain reading level, answer first. The antidote to a wall of text.",
278
+ },
279
+ {
280
+ id: "lej-output-fixer--walk-me-through-it",
281
+ sourceId: "lej-output-fixer",
282
+ path: "output-styles/walk-me-through-it.md",
283
+ displayName: "Walk Me Through It",
284
+ summary:
285
+ "Explains as it goes and assumes no prior context. The inverse of a terse preset.",
286
+ },
287
+
288
+ // ── hesreallyhim (6 of 6) ────────────────────────────────────────────────
289
+ {
290
+ id: "hesreallyhim--door-to-door-vim-salesman",
291
+ sourceId: "hesreallyhim",
292
+ path: ".claude/output-styles/door-to-door-vim-salesman.md",
293
+ displayName: "Door-to-Door Vim Salesman",
294
+ summary: "Redirects every topic toward the merits of Vim, relentlessly.",
295
+ },
296
+ {
297
+ id: "hesreallyhim--existentialist-poet",
298
+ sourceId: "hesreallyhim",
299
+ path: ".claude/output-styles/existentialist-poet.md",
300
+ // Upstream's own `name` is "Existential Poet" while the file is
301
+ // existentialist-poet.md. Recorded as upstream writes it.
302
+ displayName: "Existential Poet",
303
+ summary:
304
+ "Melancholic engineer-philosopher. Debugging as a metaphor for the human condition.",
305
+ },
306
+ {
307
+ id: "hesreallyhim--haiku-helper",
308
+ sourceId: "hesreallyhim",
309
+ path: ".claude/output-styles/haiku-helper.md",
310
+ displayName: "Haiku Helper",
311
+ summary:
312
+ "Replies only in 5-7-5 haiku. A hard constraint on length, played straight.",
313
+ },
314
+ {
315
+ id: "hesreallyhim--tabloid-journalist",
316
+ sourceId: "hesreallyhim",
317
+ path: ".claude/output-styles/tabloid-journalist.md",
318
+ displayName: "Tabloid Journalist",
319
+ summary:
320
+ "Covers your code as a scandal. Sensationalist persona, technically accurate.",
321
+ },
322
+ {
323
+ id: "hesreallyhim--technical-evangelist",
324
+ sourceId: "hesreallyhim",
325
+ path: ".claude/output-styles/technical-evangelist.md",
326
+ displayName: "Claude Code Technical Evangelist",
327
+ summary: "Enthusiastic Claude Code power user with deep product knowledge.",
328
+ },
329
+ {
330
+ id: "hesreallyhim--zen-master",
331
+ sourceId: "hesreallyhim",
332
+ path: ".claude/output-styles/zen-master.md",
333
+ displayName: "Zen Master",
334
+ summary: "Guides through koans and metaphor instead of answering directly.",
335
+ },
336
+
337
+ // ── nattergabriel (5 of 13) ──────────────────────────────────────────────
338
+ {
339
+ id: "nattergabriel--challenger",
340
+ sourceId: "nattergabriel",
341
+ path: "output-styles/challenger.md",
342
+ displayName: "Challenger",
343
+ summary:
344
+ "Challenges each decision and stress-tests a design before you commit to it.",
345
+ },
346
+ {
347
+ id: "nattergabriel--paranoid",
348
+ sourceId: "nattergabriel",
349
+ path: "output-styles/paranoid.md",
350
+ displayName: "Paranoid",
351
+ summary:
352
+ "Security-obsessed. Every input is hostile and every dependency is suspect.",
353
+ },
354
+ {
355
+ id: "nattergabriel--socratic",
356
+ sourceId: "nattergabriel",
357
+ path: "output-styles/socratic.md",
358
+ displayName: "Socratic",
359
+ summary: "Guides you to the answer with questions rather than giving it.",
360
+ },
361
+ {
362
+ id: "nattergabriel--tdd",
363
+ sourceId: "nattergabriel",
364
+ path: "output-styles/tdd.md",
365
+ displayName: "TDD",
366
+ summary:
367
+ "Test-driven. Writes the failing test before the implementation, every time.",
368
+ },
369
+ {
370
+ id: "nattergabriel--think-aloud",
371
+ sourceId: "nattergabriel",
372
+ path: "output-styles/think-aloud.md",
373
+ displayName: "Think Aloud",
374
+ summary:
375
+ "Reflects your reasoning back so you debug the problem by talking it through.",
376
+ },
377
+ // ── simple-english ───────────────────────────────────────────────────────
378
+ {
379
+ id: "simple-english--simple-english",
380
+ sourceId: "simple-english",
381
+ path: "output-styles/simple-english.md",
382
+ displayName: "Simple English",
383
+ summary:
384
+ "ASD-STE100 Simplified Technical English. One word one meaning, short sentences, no filler.",
385
+ },
386
+ ];
387
+
388
+ // ─── Exclusions, recorded so they are not silently reversed ──────────────────
389
+
390
+ /**
391
+ * Repos that ship ZERO output style files. An entry naming one would 404 on
392
+ * first fetch.
393
+ *
394
+ * They are here because two of them are the most-starred names in the field —
395
+ * `JuliusBrussee/caveman` (98,781 stars) and `blader/humanizer` (36,229) — and
396
+ * their absence reads as an oversight to anyone scanning a star column. They
397
+ * are METHODOLOGIES SHIPPED AS SKILLS. There is nothing to import. A test
398
+ * asserts no registry entry names any of them, so this decision cannot be
399
+ * quietly undone six months from now by someone sorting by popularity.
400
+ */
401
+ export const DENIED_REPOS: readonly string[] = [
402
+ "JuliusBrussee/caveman",
403
+ "blader/humanizer",
404
+ "ayghri/i-have-adhd",
405
+ "hardikpandya/stop-slop",
406
+ "petergyang/no-ai-slop",
407
+ "conorbronsdon/avoid-ai-writing",
408
+ "mattpocock/skills",
409
+ ];
410
+
411
+ /**
412
+ * Repos that DO ship importable styles but grant no redistribution licence.
413
+ *
414
+ * Deferred, not denied. `smixs` is otherwise the highest-value repo in the
415
+ * field; it is excluded only because GitHub reports NOASSERTION, and
416
+ * `SimpleClaude` carries no licence file at all. Fetching a copy to the user's
417
+ * own machine is arguably fine, but claudeup would be curating, recommending
418
+ * and automating it, and the cost of being wrong in that direction is a legal
419
+ * problem on other people's machines. The cost of being wrong in this direction
420
+ * is a style a user can still install by hand.
421
+ *
422
+ * Revisit the moment a licence appears upstream. The mechanism for shipping one
423
+ * already exists: `licence: null` renders in warning colour.
424
+ */
425
+ export const LICENCE_DEFERRED_REPOS: readonly string[] = [
426
+ "smixs/awesome-claude-output-styles",
427
+ "kylesnowschwartz/SimpleClaude",
428
+ ];
429
+
430
+ // ─── Lookup ──────────────────────────────────────────────────────────────────
431
+
432
+ /** The source a style belongs to, or null when the id names no known source. */
433
+ export function findCommunitySource(
434
+ sourceId: string,
435
+ ): CommunityStyleSource | null {
436
+ return COMMUNITY_SOURCES.find((source) => source.id === sourceId) ?? null;
437
+ }
438
+
439
+ /** A registry entry by its coordinate id (`attention-span--spartan`). */
440
+ export function findCommunityStyle(id: string): CommunityStyle | null {
441
+ return COMMUNITY_STYLES.find((style) => style.id === id) ?? null;
442
+ }
443
+
444
+ /** An entry with its source resolved, or null when either half is unknown. */
445
+ export function resolveCommunityStyle(
446
+ id: string,
447
+ ): { style: CommunityStyle; source: CommunityStyleSource } | null {
448
+ const style = findCommunityStyle(id);
449
+ if (!style) return null;
450
+ const source = findCommunitySource(style.sourceId);
451
+ if (!source) return null;
452
+ return { style, source };
453
+ }
454
+
455
+ /** Every style a source ships, in display order. */
456
+ export function stylesForSource(sourceId: string): CommunityStyle[] {
457
+ return COMMUNITY_STYLES.filter((style) => style.sourceId === sourceId).sort(
458
+ (a, b) => a.displayName.localeCompare(b.displayName),
459
+ );
460
+ }
461
+
462
+ /**
463
+ * The raw.githubusercontent.com URL for an entry.
464
+ *
465
+ * Kept here rather than in the fetcher so the one place that knows a
466
+ * coordinate's shape is the one place that knows the registry's shape.
467
+ */
468
+ export function rawUrlFor(
469
+ style: CommunityStyle,
470
+ source: CommunityStyleSource,
471
+ ): string {
472
+ return `https://raw.githubusercontent.com/${source.repo}/${source.ref}/${style.path}`;
473
+ }
474
+
475
+ /**
476
+ * Why a coordinate is unusable, or null when it is fine.
477
+ *
478
+ * These are OUR data, so a failure is a bug in a commit rather than a user
479
+ * error — callers throw on it and the registry test catches it before it ships.
480
+ * Validating anyway, because a `..` that reached the URL builder would be both
481
+ * an SSRF and a path traversal in the destination filename.
482
+ */
483
+ export function coordinateProblem(
484
+ style: CommunityStyle,
485
+ source: CommunityStyleSource,
486
+ ): string | null {
487
+ if (!isValidGitHubRepo(source.repo)) {
488
+ return `not a valid GitHub repo: "${source.repo}"`;
489
+ }
490
+ if (!/^[A-Za-z0-9._/-]+$/.test(source.ref)) {
491
+ return `not a valid git ref: "${source.ref}"`;
492
+ }
493
+ if (!/^[A-Za-z0-9._/-]+\.md$/.test(style.path)) {
494
+ return `not a valid style path: "${style.path}"`;
495
+ }
496
+ if (style.path.includes("..") || style.path.startsWith("/")) {
497
+ return `style path escapes the repo: "${style.path}"`;
498
+ }
499
+ if (!style.path.startsWith(`${source.dir}/`)) {
500
+ return `"${style.path}" is not under "${source.dir}/"`;
501
+ }
502
+ // The id is also the cache filename, so anything that is not a plain slug
503
+ // pair would escape the cache directory or collide with another entry.
504
+ if (
505
+ !new RegExp(`^[a-z0-9-]+${COMMUNITY_ID_SEPARATOR}[a-z0-9-]+$`).test(
506
+ style.id,
507
+ )
508
+ ) {
509
+ return `id "${style.id}" is not a <source>${COMMUNITY_ID_SEPARATOR}<style> slug pair`;
510
+ }
511
+ if (style.id !== `${source.id}${COMMUNITY_ID_SEPARATOR}${slugOf(style.id)}`) {
512
+ return `id "${style.id}" does not start with "${source.id}${COMMUNITY_ID_SEPARATOR}"`;
513
+ }
514
+ return null;
515
+ }
516
+
517
+ /** The style half of a coordinate id. `attention-span--spartan` -> `spartan`. */
518
+ export function slugOf(id: string): string {
519
+ const at = id.indexOf(COMMUNITY_ID_SEPARATOR);
520
+ return at === -1 ? id : id.slice(at + COMMUNITY_ID_SEPARATOR.length);
521
+ }
package/src/main.tsx CHANGED
@@ -33,6 +33,21 @@ async function main(): Promise<void> {
33
33
  // element now names an adaptive colour explicitly — see src/ui/theme.ts.
34
34
  const renderer = await createCliRenderer({
35
35
  backgroundColor: RGBA.defaultBackground(),
36
+ // claudeup is keyboard-only — no component sets onMouseDown/Up/Move, and
37
+ // every action has a key. OpenTUI nevertheless defaults BOTH of these to
38
+ // true (`config.useMouse ?? true`, `config.enableMouseMovement ?? true`),
39
+ // which turns on button reporting and mode-1003 "report all motion".
40
+ //
41
+ // Inside tmux that is actively harmful: once a pane's application asks for
42
+ // mouse events, tmux hands them to that application instead of using them
43
+ // to select a pane or scroll history. Clicking another pane then does
44
+ // nothing, keyboard focus stays where it was, and what you type lands in
45
+ // the pane you thought you had just left.
46
+ //
47
+ // Turning it off costs nothing here and gives clicking and scrolling back
48
+ // to the terminal.
49
+ useMouse: false,
50
+ enableMouseMovement: false,
36
51
  });
37
52
 
38
53
  // Ask the terminal whether it is light or dark, once. Only the disabled-row