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.
Files changed (33) hide show
  1. package/package.json +4 -4
  2. package/scripts/verify-community-registry.ts +272 -0
  3. package/src/__tests__/community-fetch.test.ts +545 -0
  4. package/src/__tests__/community-registry.test.ts +269 -0
  5. package/src/__tests__/community-staleness.test.ts +722 -0
  6. package/src/__tests__/open-file.test.ts +59 -0
  7. package/src/__tests__/style-wrap.test.ts +220 -0
  8. package/src/__tests__/styles-manager.test.ts +1124 -0
  9. package/src/__tests__/styles-origins.test.ts +416 -0
  10. package/src/__tests__/styles-screen-state.test.ts +460 -0
  11. package/src/__tests__/styles-status-line.test.ts +72 -0
  12. package/src/__tests__/styles-sync.test.ts +452 -0
  13. package/src/__tests__/tabbar-layout.test.ts +62 -0
  14. package/src/__tests__/terminology-filler.test.ts +214 -0
  15. package/src/data/community-styles.ts +521 -0
  16. package/src/main.tsx +15 -0
  17. package/src/services/catalog-cache-store.ts +101 -7
  18. package/src/services/community-fetcher.ts +90 -0
  19. package/src/services/community-styles.ts +1194 -0
  20. package/src/services/styles-manager.ts +1400 -0
  21. package/src/services/terminology-filler.ts +266 -0
  22. package/src/ui/App.tsx +15 -3
  23. package/src/ui/adapters/stylesAdapter.ts +403 -0
  24. package/src/ui/components/TabBar.tsx +43 -9
  25. package/src/ui/components/primitives/ActionHints.tsx +4 -1
  26. package/src/ui/components/primitives/ListCategoryRow.tsx +10 -1
  27. package/src/ui/registry.ts +6 -0
  28. package/src/ui/renderers/styleRenderers.tsx +809 -0
  29. package/src/ui/screens/StylesScreen.tsx +1089 -0
  30. package/src/ui/screens/index.ts +1 -0
  31. package/src/ui/state/reducer.ts +113 -1
  32. package/src/ui/state/types.ts +60 -2
  33. package/src/utils/open-file.ts +84 -0
@@ -5,5 +5,6 @@ export { SettingsScreen } from "./EnvVarsScreen.js";
5
5
  export { CliToolsScreen } from "./CliToolsScreen.js";
6
6
  export { ProfilesScreen } from "./ProfilesScreen.js";
7
7
  export { SkillsScreen } from "./SkillsScreen.js";
8
+ export { StylesScreen } from "./StylesScreen.js";
8
9
  export { GitignoreScreen } from "./GitignoreScreen.js";
9
10
  export { AliasScreen } from "./AliasScreen.js";
@@ -1,4 +1,4 @@
1
- import type { AppState, AppAction } from "./types.js";
1
+ import type { AppAction, AppState } from "./types.js";
2
2
  import { asyncValue } from "./types.js";
3
3
 
4
4
  export const initialState: AppState = {
@@ -61,6 +61,15 @@ export const initialState: AppState = {
61
61
  skills: { status: "idle" },
62
62
  updateStatus: null,
63
63
  },
64
+
65
+ styles: {
66
+ selectedIndex: 0,
67
+ searchQuery: "",
68
+ snapshot: { status: "idle" },
69
+ selected: new Set(),
70
+ status: null,
71
+ isFilling: false,
72
+ },
64
73
  };
65
74
 
66
75
  export function appReducer(state: AppState, action: AppAction): AppState {
@@ -486,6 +495,109 @@ export function appReducer(state: AppState, action: AppAction): AppState {
486
495
  };
487
496
  }
488
497
 
498
+ // =========================================================================
499
+ // Styles screen
500
+ // =========================================================================
501
+ case "STYLES_SELECT":
502
+ return {
503
+ ...state,
504
+ styles: { ...state.styles, selectedIndex: action.index },
505
+ };
506
+
507
+ case "STYLES_SET_SEARCH":
508
+ return {
509
+ ...state,
510
+ styles: { ...state.styles, searchQuery: action.query },
511
+ };
512
+
513
+ case "STYLES_SEARCH_APPEND":
514
+ return {
515
+ ...state,
516
+ styles: {
517
+ ...state.styles,
518
+ searchQuery: state.styles.searchQuery + action.char,
519
+ selectedIndex: 0,
520
+ },
521
+ };
522
+
523
+ case "STYLES_SEARCH_BACKSPACE":
524
+ return {
525
+ ...state,
526
+ styles: {
527
+ ...state.styles,
528
+ searchQuery: state.styles.searchQuery.slice(0, -1),
529
+ selectedIndex: 0,
530
+ },
531
+ };
532
+
533
+ case "STYLES_DATA_LOADING":
534
+ return {
535
+ ...state,
536
+ styles: { ...state.styles, snapshot: { status: "loading" } },
537
+ };
538
+
539
+ case "STYLES_DATA_SUCCESS":
540
+ return {
541
+ ...state,
542
+ styles: {
543
+ ...state.styles,
544
+ snapshot: { status: "success", data: action.snapshot },
545
+ // Seed from the project's committed declaration when there is
546
+ // one, falling back to whatever is live locally.
547
+ //
548
+ // The declaration wins deliberately. It is what the project asked
549
+ // for; the local artifact is only what this machine last did. When
550
+ // the two differ the screen says "press a to re-apply" — and that
551
+ // sentence is a lie if `a` would re-apply the stale local set
552
+ // instead of the pulled one.
553
+ selected: new Set(
554
+ action.snapshot.declaration
555
+ ? [
556
+ ...action.snapshot.declaration.presets,
557
+ ...action.snapshot.declaration.imports,
558
+ ]
559
+ : [
560
+ ...(action.snapshot.applied?.presets ?? []),
561
+ ...(action.snapshot.applied?.imports ?? []),
562
+ ],
563
+ ),
564
+ },
565
+ };
566
+
567
+ case "STYLES_DATA_ERROR":
568
+ return {
569
+ ...state,
570
+ styles: {
571
+ ...state.styles,
572
+ snapshot: { status: "error", error: action.error },
573
+ },
574
+ };
575
+
576
+ case "STYLES_STATUS_SET":
577
+ return { ...state, styles: { ...state.styles, status: action.status } };
578
+
579
+ case "STYLES_STATUS_CLEAR":
580
+ return { ...state, styles: { ...state.styles, status: null } };
581
+
582
+ case "STYLES_FILL_START":
583
+ return { ...state, styles: { ...state.styles, isFilling: true } };
584
+
585
+ case "STYLES_FILL_END":
586
+ return { ...state, styles: { ...state.styles, isFilling: false } };
587
+
588
+ case "STYLES_TOGGLE": {
589
+ const selected = new Set(state.styles.selected);
590
+ if (selected.has(action.id)) selected.delete(action.id);
591
+ else selected.add(action.id);
592
+ return { ...state, styles: { ...state.styles, selected } };
593
+ }
594
+
595
+ case "STYLES_SET_SELECTION":
596
+ return {
597
+ ...state,
598
+ styles: { ...state.styles, selected: new Set(action.ids) },
599
+ };
600
+
489
601
  // =========================================================================
490
602
  // Modals
491
603
  // =========================================================================
@@ -1,11 +1,12 @@
1
+ import type { PluginInfo } from "../../services/plugin-manager.js";
2
+ import type { VersionMismatchInfo } from "../../services/plugin-version-check.js";
3
+ import type { StylesSnapshot } from "../../services/styles-manager.js";
1
4
  import type {
2
5
  Marketplace,
3
6
  McpServer,
4
7
  ProfileEntry,
5
8
  SkillInfo,
6
9
  } from "../../types/index.js";
7
- import type { PluginInfo } from "../../services/plugin-manager.js";
8
- import type { VersionMismatchInfo } from "../../services/plugin-version-check.js";
9
10
 
10
11
  // ============================================================================
11
12
  // Route Types
@@ -19,6 +20,7 @@ export type Screen =
19
20
  | "cli-tools"
20
21
  | "profiles"
21
22
  | "skills"
23
+ | "styles"
22
24
  | "gitignore"
23
25
  | "alias";
24
26
 
@@ -30,6 +32,7 @@ export type Route =
30
32
  | { screen: "cli-tools" }
31
33
  | { screen: "profiles" }
32
34
  | { screen: "skills" }
35
+ | { screen: "styles" }
33
36
  | { screen: "gitignore" }
34
37
  | { screen: "alias" };
35
38
 
@@ -191,6 +194,38 @@ export interface SkillsScreenState {
191
194
  updateStatus: Map<string, boolean> | null;
192
195
  }
193
196
 
197
+ export interface StylesScreenState {
198
+ selectedIndex: number;
199
+ searchQuery: string;
200
+ /** Presets + importable styles + what is currently live, in one read. */
201
+ snapshot: AsyncData<StylesSnapshot>;
202
+ /**
203
+ * Pending selection, by source id. Staged rather than applied on each
204
+ * keystroke: composing writes a file and flips the harness's active output
205
+ * style, which is not something a stray Space press should do.
206
+ */
207
+ selected: Set<string>;
208
+ /**
209
+ * The status line's current message, or null for the default line.
210
+ *
211
+ * App state for the same reason `isFilling` is: `Router` swaps the component
212
+ * type on a tab change, so anything held in the screen's own `useState` is
213
+ * destroyed by `9 → 1 → 9`. It also stops being cleared on a wall clock —
214
+ * the message now survives until the next action replaces it, so the last
215
+ * thing you did is still on screen whenever you look.
216
+ */
217
+ status: { text: string; tone: "success" | "error" } | null;
218
+ /**
219
+ * True while a template fill (Claude Code over the whole project) is running.
220
+ *
221
+ * App state, not screen state, because the screen UNMOUNTS when you switch
222
+ * tabs. A fill runs for minutes, so switching away is the normal thing to do
223
+ * — and with the flag held locally the returning user saw no sign one was in
224
+ * flight and could launch a second subprocess on top of the first.
225
+ */
226
+ isFilling: boolean;
227
+ }
228
+
194
229
  // ============================================================================
195
230
  // App State
196
231
  // ============================================================================
@@ -224,6 +259,7 @@ export interface AppState {
224
259
  cliTools: CliToolsScreenState;
225
260
  profiles: ProfilesScreenState;
226
261
  skills: SkillsScreenState;
262
+ styles: StylesScreenState;
227
263
  }
228
264
 
229
265
  // ============================================================================
@@ -312,5 +348,27 @@ export type AppAction =
312
348
  | { type: "SKILLS_UPDATE_STATUS"; updates: Map<string, boolean> }
313
349
  | { type: "SKILLS_UPDATE_ITEM"; name: string; updates: Partial<SkillInfo> }
314
350
 
351
+ // Styles screen
352
+ | { type: "STYLES_SELECT"; index: number }
353
+ | { type: "STYLES_SET_SEARCH"; query: string }
354
+ // Append/backspace are computed IN the reducer, from current state. A
355
+ // screen-side `searchQuery + char` reads a value captured at render time, so
356
+ // keystrokes arriving faster than React re-renders all overwrite each other
357
+ // and typing "slop" leaves just "p".
358
+ | { type: "STYLES_SEARCH_APPEND"; char: string }
359
+ | { type: "STYLES_SEARCH_BACKSPACE" }
360
+ | { type: "STYLES_DATA_LOADING" }
361
+ | { type: "STYLES_DATA_SUCCESS"; snapshot: StylesSnapshot }
362
+ | { type: "STYLES_DATA_ERROR"; error: Error }
363
+ | { type: "STYLES_TOGGLE"; id: string }
364
+ | {
365
+ type: "STYLES_STATUS_SET";
366
+ status: { text: string; tone: "success" | "error" };
367
+ }
368
+ | { type: "STYLES_STATUS_CLEAR" }
369
+ | { type: "STYLES_FILL_START" }
370
+ | { type: "STYLES_FILL_END" }
371
+ | { type: "STYLES_SET_SELECTION"; ids: string[] }
372
+
315
373
  // Data refresh - triggers screens to refetch
316
374
  | { type: "DATA_REFRESH_COMPLETE" };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Hand a file or URL to the operating system's default application.
3
+ *
4
+ * Why this exists rather than launching `$EDITOR`: claudeup owns the terminal.
5
+ * Suspending an OpenTUI app to give the TTY to a full-screen editor and then
6
+ * restoring raw mode, the alternate screen buffer and the cursor is a reliable
7
+ * way to leave the terminal wedged if anything goes wrong in between. Handing
8
+ * the path to the OS launches a separate process that never touches our TTY.
9
+ */
10
+
11
+ import { spawn } from "node:child_process";
12
+
13
+ export interface Opener {
14
+ command: string;
15
+ /** Arguments that precede the target path. */
16
+ args: string[];
17
+ }
18
+
19
+ /**
20
+ * The platform's "open this with whatever handles it" command.
21
+ *
22
+ * claudeup ships darwin-arm64, darwin-x64 and linux-x64, so macOS and Linux
23
+ * both have to work — the existing `open "${url}"` call in the Skills screen is
24
+ * macOS-only and silently does nothing on Linux.
25
+ */
26
+ export function openerFor(platform: NodeJS.Platform): Opener {
27
+ if (platform === "darwin") return { command: "open", args: [] };
28
+ // `start` is a cmd.exe builtin, and its first quoted argument is the window
29
+ // title — hence the empty string, or a quoted path would be consumed as one.
30
+ if (platform === "win32")
31
+ return { command: "cmd", args: ["/c", "start", ""] };
32
+ return { command: "xdg-open", args: [] };
33
+ }
34
+
35
+ /**
36
+ * Open `target` in the default application, without blocking the TUI.
37
+ *
38
+ * The child is spawned WITHOUT a shell and the target is passed as its own
39
+ * argv entry, so a filename containing a space, quote or `$(…)` is opened
40
+ * rather than interpreted. Resolves once the process has been spawned, not
41
+ * when it exits — the viewer outlives us by design.
42
+ */
43
+ export function openInDefaultApp(
44
+ target: string,
45
+ platform: NodeJS.Platform = process.platform,
46
+ /**
47
+ * Test seam. The default is the platform's real opener; a test passes a
48
+ * command that cannot exist, so the missing-opener path is exercised
49
+ * deterministically on every OS — CI runners ship `xdg-open`, so "assume
50
+ * the opener is absent" fails exactly there, and "spawn the real one"
51
+ * launches applications on a build machine.
52
+ */
53
+ opener: Opener = openerFor(platform),
54
+ ): Promise<void> {
55
+ const { command, args } = opener;
56
+
57
+ return new Promise((resolve, reject) => {
58
+ let settled = false;
59
+ const child = spawn(command, [...args, target], {
60
+ detached: true,
61
+ stdio: "ignore",
62
+ });
63
+
64
+ child.once("error", (error: NodeJS.ErrnoException) => {
65
+ if (settled) return;
66
+ settled = true;
67
+ reject(
68
+ error.code === "ENOENT"
69
+ ? new Error(
70
+ `No way to open files here — "${command}" is not on PATH.`,
71
+ )
72
+ : error,
73
+ );
74
+ });
75
+
76
+ child.once("spawn", () => {
77
+ if (settled) return;
78
+ settled = true;
79
+ // Detach so claudeup exiting does not take the editor with it.
80
+ child.unref();
81
+ resolve();
82
+ });
83
+ });
84
+ }