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
package/src/ui/screens/index.ts
CHANGED
|
@@ -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";
|
package/src/ui/state/reducer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
// =========================================================================
|
package/src/ui/state/types.ts
CHANGED
|
@@ -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
|
+
}
|