claudeup 4.39.0 → 4.40.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/README.md +84 -1
- package/package.json +4 -4
- package/scripts/capture-builtin.ts +412 -0
- package/src/__tests__/capture-builtin.test.ts +92 -0
- package/src/__tests__/styles-manager.test.ts +75 -101
- package/src/__tests__/styles-screen-state.test.ts +0 -3
- package/src/__tests__/terminology-filler.test.ts +2 -2
- package/src/data/community-styles.ts +3 -3
- package/src/data/styles/asd-ste100.md +38 -0
- package/src/data/styles/calibrated.md +24 -0
- package/src/data/styles/direct.md +23 -0
- package/src/data/styles/evidence-first.md +22 -0
- package/src/data/styles/explanatory.md +23 -0
- package/src/data/styles/index.ts +57 -0
- package/src/data/styles/no-slop.md +32 -0
- package/src/data/styles/plain-language.md +23 -0
- package/src/data/styles/structured.md +30 -0
- package/src/data/styles/terminology.md +28 -0
- package/src/data/styles/terse.md +20 -0
- package/src/markdown.d.ts +13 -0
- package/src/services/styles-manager.ts +42 -126
- package/src/ui/components/CategoryHeader.tsx +28 -11
- package/src/ui/components/ScrollableList.tsx +8 -0
- package/src/ui/screens/StylesScreen.tsx +5 -31
|
@@ -3,23 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ## What a "style" is
|
|
5
5
|
*
|
|
6
|
-
* Claude Code activates exactly ONE output style at a time. The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Claude Code activates exactly ONE output style at a time. The preset model
|
|
7
|
+
* is compositional instead — one verbosity preset plus any number of
|
|
8
|
+
* modifiers — so the combination has to be flattened into a single generated
|
|
9
|
+
* style file before the harness ever sees it. That flattening is what
|
|
10
|
+
* `composeStyleFile` does here.
|
|
11
11
|
*
|
|
12
|
-
* ##
|
|
12
|
+
* ## Where the presets come from
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* contract, and reading it needs no runtime at all.
|
|
20
|
-
*
|
|
21
|
-
* Parity with the plugin is asserted in `__tests__/styles-manager.test.ts`
|
|
22
|
-
* against the real shipped preset files.
|
|
14
|
+
* `data/styles/*.md`, embedded in the binary at build time. They used to ship
|
|
15
|
+
* in the `style@magus` plugin and be discovered on disk, which meant two
|
|
16
|
+
* implementations of one composer and an empty Styles tab for anyone without
|
|
17
|
+
* the plugin. The plugin was retired at Marketplace 10.0.0; this is now the
|
|
18
|
+
* only composer, and the presets travel with the binary that parses them.
|
|
23
19
|
*
|
|
24
20
|
* ## Profile awareness
|
|
25
21
|
*
|
|
@@ -40,6 +36,7 @@ import os from "node:os";
|
|
|
40
36
|
import path from "node:path";
|
|
41
37
|
import fs from "fs-extra";
|
|
42
38
|
import { findCommunityStyle } from "../data/community-styles.js";
|
|
39
|
+
import { EMBEDDED_PRESETS } from "../data/styles/index.js";
|
|
43
40
|
import { getManifestPath, readManifest, writeManifest } from "./manifest.js";
|
|
44
41
|
import { activeProfile } from "./symlink-manager.js";
|
|
45
42
|
|
|
@@ -55,7 +52,7 @@ export const COMMUNITY_ID_PREFIX = "community:";
|
|
|
55
52
|
|
|
56
53
|
export type StyleAxis = "verbosity" | "modifier";
|
|
57
54
|
|
|
58
|
-
/** A preset
|
|
55
|
+
/** A preset embedded in this binary, from `data/styles/`. */
|
|
59
56
|
export interface StylePreset {
|
|
60
57
|
kind: "preset";
|
|
61
58
|
/** Selection key. Equals `name` — presets share one namespace. */
|
|
@@ -74,6 +71,11 @@ export interface StylePreset {
|
|
|
74
71
|
/** Template presets ship an empty table and cannot be applied directly. */
|
|
75
72
|
template: boolean;
|
|
76
73
|
body: string;
|
|
74
|
+
/**
|
|
75
|
+
* What the detail panel shows under "Source". A label rather than a real
|
|
76
|
+
* path: an embedded preset has no file on disk, and the open-in-editor
|
|
77
|
+
* action refuses presets before it ever reads this.
|
|
78
|
+
*/
|
|
77
79
|
path: string;
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -81,9 +83,9 @@ export interface StylePreset {
|
|
|
81
83
|
* Where an importable style came from, which is what decides how it is grouped
|
|
82
84
|
* and whether it travels with the repository.
|
|
83
85
|
*
|
|
84
|
-
* - `anthropic` — a Claude Code built-in captured by
|
|
85
|
-
* `capture-builtin.ts`. Its text ships inside the
|
|
86
|
-
* snapshot that goes stale on the next upgrade.
|
|
86
|
+
* - `anthropic` — a Claude Code built-in captured by
|
|
87
|
+
* `scripts/capture-builtin.ts`. Its text ships inside the Claude Code
|
|
88
|
+
* binary, so the file is a snapshot that goes stale on the next upgrade.
|
|
87
89
|
* - `team` — project-scoped, so it lives in the repo and commits with it. This
|
|
88
90
|
* is the one everyone on the project gets.
|
|
89
91
|
* - `personal` — user-scoped, so it exists only on this machine.
|
|
@@ -180,8 +182,6 @@ export interface StylesSnapshot {
|
|
|
180
182
|
settingsPath: string;
|
|
181
183
|
/** Name of the generated style, e.g. "composed" or "composed-dev". */
|
|
182
184
|
styleName: string;
|
|
183
|
-
/** Directory presets were read from; null when style@magus isn't installed. */
|
|
184
|
-
presetsRoot: string | null;
|
|
185
185
|
/** Active profile name, when .claude/settings.json is a profile symlink. */
|
|
186
186
|
profile: string | null;
|
|
187
187
|
/** The value currently in settings.json, whatever set it. */
|
|
@@ -266,7 +266,8 @@ type Frontmatter = Record<string, string>;
|
|
|
266
266
|
*
|
|
267
267
|
* Deliberately not a YAML parser: the keys we read are flat scalars, and
|
|
268
268
|
* pulling in a YAML dependency to read five strings would be the tail wagging
|
|
269
|
-
* the dog.
|
|
269
|
+
* the dog. Quote-unescaping is included because a `description:` containing a
|
|
270
|
+
* colon has to be quoted to stay valid YAML for every other reader.
|
|
270
271
|
*/
|
|
271
272
|
export function splitFrontmatter(text: string): {
|
|
272
273
|
frontmatter: Frontmatter;
|
|
@@ -346,106 +347,22 @@ async function markdownFiles(dir: string): Promise<string[]> {
|
|
|
346
347
|
}
|
|
347
348
|
}
|
|
348
349
|
|
|
349
|
-
// ───
|
|
350
|
-
|
|
351
|
-
/** Newest-first semver-ish comparison. Non-numeric segments sort last. */
|
|
352
|
-
function compareVersionsDesc(a: string, b: string): number {
|
|
353
|
-
const parse = (v: string) =>
|
|
354
|
-
v.split(".").map((part) => {
|
|
355
|
-
const n = Number.parseInt(part, 10);
|
|
356
|
-
return Number.isNaN(n) ? -1 : n;
|
|
357
|
-
});
|
|
358
|
-
const av = parse(a);
|
|
359
|
-
const bv = parse(b);
|
|
360
|
-
for (let i = 0; i < Math.max(av.length, bv.length); i++) {
|
|
361
|
-
const diff = (bv[i] ?? 0) - (av[i] ?? 0);
|
|
362
|
-
if (diff !== 0) return diff;
|
|
363
|
-
}
|
|
364
|
-
return 0;
|
|
365
|
-
}
|
|
350
|
+
// ─── Presets ────────────────────────────────────────────────────────────────
|
|
366
351
|
|
|
367
352
|
/**
|
|
368
|
-
*
|
|
353
|
+
* Parse the embedded preset files into usable presets.
|
|
369
354
|
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* Returns null when style@magus isn't installed; the screen renders an empty
|
|
376
|
-
* state rather than pretending there are no styles.
|
|
355
|
+
* Synchronous and infallible by construction: the text is inlined at build
|
|
356
|
+
* time, so there is no directory to miss, no plugin to be uninstalled, and no
|
|
357
|
+
* version of the files other than the one this binary was built with. The
|
|
358
|
+
* frontmatter contract is `name`, `title`, `axis`, `summary`, `conflicts`,
|
|
359
|
+
* `template` — see `data/styles/index.ts`.
|
|
377
360
|
*/
|
|
378
|
-
export
|
|
379
|
-
const home = os.homedir();
|
|
380
|
-
const cacheRoot = path.join(home, ".claude", "plugins", "cache");
|
|
381
|
-
|
|
382
|
-
// 1. Installed plugin cache: cache/<marketplace>/style/<version>/styles
|
|
383
|
-
try {
|
|
384
|
-
const marketplaces = await fs.readdir(cacheRoot);
|
|
385
|
-
const candidates: Array<{ version: string; dir: string }> = [];
|
|
386
|
-
for (const marketplace of marketplaces) {
|
|
387
|
-
const styleDir = path.join(cacheRoot, marketplace, "style");
|
|
388
|
-
if (!(await fs.pathExists(styleDir))) continue;
|
|
389
|
-
for (const version of await fs.readdir(styleDir)) {
|
|
390
|
-
const dir = path.join(styleDir, version, "styles");
|
|
391
|
-
if (await fs.pathExists(dir)) candidates.push({ version, dir });
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
if (candidates.length > 0) {
|
|
395
|
-
candidates.sort((a, b) => compareVersionsDesc(a.version, b.version));
|
|
396
|
-
return candidates[0].dir;
|
|
397
|
-
}
|
|
398
|
-
} catch {
|
|
399
|
-
/* no cache dir — fall through */
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// 2. Marketplace clone
|
|
403
|
-
const marketplacesRoot = path.join(
|
|
404
|
-
home,
|
|
405
|
-
".claude",
|
|
406
|
-
"plugins",
|
|
407
|
-
"marketplaces",
|
|
408
|
-
);
|
|
409
|
-
try {
|
|
410
|
-
for (const marketplace of await fs.readdir(marketplacesRoot)) {
|
|
411
|
-
const dir = path.join(
|
|
412
|
-
marketplacesRoot,
|
|
413
|
-
marketplace,
|
|
414
|
-
"plugins",
|
|
415
|
-
"style",
|
|
416
|
-
"styles",
|
|
417
|
-
);
|
|
418
|
-
if (await fs.pathExists(dir)) return dir;
|
|
419
|
-
}
|
|
420
|
-
} catch {
|
|
421
|
-
/* no marketplaces dir — fall through */
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// 3. Running from source: walk up looking for plugins/style/styles
|
|
425
|
-
try {
|
|
426
|
-
let dir = path.dirname(new URL(import.meta.url).pathname);
|
|
427
|
-
for (let depth = 0; depth < 8; depth++) {
|
|
428
|
-
const candidate = path.join(dir, "plugins", "style", "styles");
|
|
429
|
-
if (await fs.pathExists(candidate)) return candidate;
|
|
430
|
-
const parent = path.dirname(dir);
|
|
431
|
-
if (parent === dir) break;
|
|
432
|
-
dir = parent;
|
|
433
|
-
}
|
|
434
|
-
} catch {
|
|
435
|
-
/* not resolvable in a compiled binary — fine */
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async function readPresets(root: string | null): Promise<StylePreset[]> {
|
|
442
|
-
if (!root) return [];
|
|
361
|
+
export function loadPresets(): StylePreset[] {
|
|
443
362
|
const presets: StylePreset[] = [];
|
|
444
|
-
for (const file of
|
|
445
|
-
const { frontmatter, body } = splitFrontmatter(
|
|
446
|
-
|
|
447
|
-
);
|
|
448
|
-
const name = frontmatter.name || path.basename(file, ".md");
|
|
363
|
+
for (const { file, text } of EMBEDDED_PRESETS) {
|
|
364
|
+
const { frontmatter, body } = splitFrontmatter(text);
|
|
365
|
+
const name = frontmatter.name || file.replace(/\.md$/, "");
|
|
449
366
|
presets.push({
|
|
450
367
|
kind: "preset",
|
|
451
368
|
id: name,
|
|
@@ -456,7 +373,7 @@ async function readPresets(root: string | null): Promise<StylePreset[]> {
|
|
|
456
373
|
conflicts: splitList(frontmatter.conflicts),
|
|
457
374
|
template: asBool(frontmatter.template) === true,
|
|
458
375
|
body,
|
|
459
|
-
path: file
|
|
376
|
+
path: `bundled with claudeup · styles/${file}`,
|
|
460
377
|
});
|
|
461
378
|
}
|
|
462
379
|
return presets;
|
|
@@ -892,8 +809,7 @@ export interface LoadStylesOptions {
|
|
|
892
809
|
/**
|
|
893
810
|
* Home directory to read user-scoped styles from. Defaults to the real one.
|
|
894
811
|
* Overridable so a test does not depend on whatever the developer happens to
|
|
895
|
-
* have in ~/.claude/output-styles
|
|
896
|
-
* `--home` flag for the same reason.
|
|
812
|
+
* have in ~/.claude/output-styles.
|
|
897
813
|
*/
|
|
898
814
|
home?: string;
|
|
899
815
|
}
|
|
@@ -909,8 +825,7 @@ export async function loadStyles(
|
|
|
909
825
|
const stylePath = stylePathFor(projectPath, styleName);
|
|
910
826
|
const settingsPath = settingsPathFor(projectPath);
|
|
911
827
|
|
|
912
|
-
const
|
|
913
|
-
const presets = await readPresets(presetsRoot);
|
|
828
|
+
const presets = loadPresets();
|
|
914
829
|
// Exclude every name we might generate, not just the current one — a style
|
|
915
830
|
// left over from another profile is ours, not the user's, and importing it
|
|
916
831
|
// would nest a composition inside a composition.
|
|
@@ -946,7 +861,6 @@ export async function loadStyles(
|
|
|
946
861
|
stylePath,
|
|
947
862
|
settingsPath,
|
|
948
863
|
styleName,
|
|
949
|
-
presetsRoot,
|
|
950
864
|
profile,
|
|
951
865
|
currentOutputStyle,
|
|
952
866
|
};
|
|
@@ -1036,9 +950,11 @@ const MAX_DESCRIPTION = 200;
|
|
|
1036
950
|
* specific-after-broad, so later text refines earlier text rather than being
|
|
1037
951
|
* buried by it.
|
|
1038
952
|
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
953
|
+
* This is the only copy. It used to be duplicated in the retired style
|
|
954
|
+
* plugin's composer, with a test comparing the two; both are gone, so nothing
|
|
955
|
+
* can drift from it any more. It is still asserted to reach EVERY composition
|
|
956
|
+
* in `__tests__/styles-manager.test.ts` — that check is what matters, since
|
|
957
|
+
* the block is the security backstop on arbitrary assembled instruction text.
|
|
1042
958
|
*/
|
|
1043
959
|
export const INTEGRITY_BLOCK = `## Style limits
|
|
1044
960
|
|
|
@@ -29,18 +29,35 @@ export function CategoryHeader({
|
|
|
29
29
|
const countBadge = count !== undefined ? ` (${count})` : "";
|
|
30
30
|
const statusText = status ? ` ${status}` : "";
|
|
31
31
|
|
|
32
|
-
//
|
|
32
|
+
// Flex row instead of one flat <text>: a flat text wraps when the pane is
|
|
33
|
+
// narrower than the row ("by MadAppGang" lost its final character and the wrap
|
|
34
|
+
// shifted every item below by one line). Here the dash filler is the only
|
|
35
|
+
// element allowed to shrink to nothing, so the badge survives at any width
|
|
36
|
+
// and the title clips last.
|
|
33
37
|
return (
|
|
34
|
-
<
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
<box flexDirection="row" width="100%" height={1} overflow="hidden">
|
|
39
|
+
<box flexShrink={1} minWidth={4} overflow="hidden" height={1}>
|
|
40
|
+
<text fg={theme.colors.text}>
|
|
41
|
+
<span fg={theme.colors.muted}>{expandIcon}</span>
|
|
42
|
+
<span fg={theme.colors.text}>
|
|
43
|
+
<strong> {title}</strong>
|
|
44
|
+
</span>
|
|
45
|
+
<span fg={theme.colors.muted}>{versionBadge}</span>
|
|
46
|
+
<span fg={theme.colors.muted}>{countBadge}</span>
|
|
47
|
+
</text>
|
|
48
|
+
</box>
|
|
49
|
+
{/* flexShrink 100: the filler must be the first thing sacrificed. With equal
|
|
50
|
+
shrink factors yoga takes width from the (larger-basis) title first, which
|
|
51
|
+
clipped a character off the title while all four dashes survived. */}
|
|
52
|
+
<box flexShrink={100} minWidth={0} overflow="hidden" height={1}>
|
|
53
|
+
<text fg={theme.colors.border}> ────</text>
|
|
54
|
+
</box>
|
|
55
|
+
{status ? (
|
|
56
|
+
<box flexShrink={0} height={1}>
|
|
57
|
+
<text fg={statusColor}>{statusText}</text>
|
|
58
|
+
</box>
|
|
59
|
+
) : null}
|
|
60
|
+
</box>
|
|
44
61
|
);
|
|
45
62
|
}
|
|
46
63
|
|
|
@@ -70,6 +70,14 @@ export function ScrollableList<T>({
|
|
|
70
70
|
<box
|
|
71
71
|
key={getKey ? getKey(item, originalIndex) : `${originalIndex}`}
|
|
72
72
|
width="100%"
|
|
73
|
+
// height=1 is load-bearing, not cosmetic. overflow="hidden" alone clips
|
|
74
|
+
// CONTENT but lets the box GROW: an over-wide <text> wraps to a second
|
|
75
|
+
// line, the row becomes two lines tall, and every item below shifts —
|
|
76
|
+
// which rendered as a phantom blank line under "Magus Marketing" and
|
|
77
|
+
// the last two plugins composited onto one row. The list's scroll math
|
|
78
|
+
// assumes one line per item; this makes that assumption true.
|
|
79
|
+
height={1}
|
|
80
|
+
flexShrink={0}
|
|
73
81
|
overflow="hidden"
|
|
74
82
|
>
|
|
75
83
|
{renderItem(item, originalIndex, originalIndex === selectedIndex)}
|
|
@@ -628,9 +628,9 @@ export function StylesScreen() {
|
|
|
628
628
|
/**
|
|
629
629
|
* Open the selected style's file in the system's default application.
|
|
630
630
|
*
|
|
631
|
-
* Presets are excluded on purpose: they
|
|
632
|
-
*
|
|
633
|
-
*
|
|
631
|
+
* Presets are excluded on purpose: they are compiled into this binary and
|
|
632
|
+
* have no file on disk to open. Saying so is more useful than failing on a
|
|
633
|
+
* path that does not exist.
|
|
634
634
|
*/
|
|
635
635
|
const handleOpen = useCallback(async () => {
|
|
636
636
|
if (selectedItem?.kind === "offer") {
|
|
@@ -645,7 +645,7 @@ export function StylesScreen() {
|
|
|
645
645
|
|
|
646
646
|
if (source.kind === "preset") {
|
|
647
647
|
showStatus(
|
|
648
|
-
`${source.name}
|
|
648
|
+
`${source.name} is built into claudeup and is replaced on update — press n to make a team style instead`,
|
|
649
649
|
"error",
|
|
650
650
|
);
|
|
651
651
|
return;
|
|
@@ -943,7 +943,6 @@ export function StylesScreen() {
|
|
|
943
943
|
Math.floor(dimensions.terminalWidth * 0.5) - 4,
|
|
944
944
|
);
|
|
945
945
|
const query = stylesState.searchQuery.trim();
|
|
946
|
-
const noPresets = snapshot !== null && snapshot.presetsRoot === null;
|
|
947
946
|
// `t` only means anything on a template preset, so it is only advertised
|
|
948
947
|
// there — a footer full of keys that do nothing on the current row teaches
|
|
949
948
|
// the wrong thing.
|
|
@@ -1039,30 +1038,6 @@ export function StylesScreen() {
|
|
|
1039
1038
|
</box>
|
|
1040
1039
|
)}
|
|
1041
1040
|
|
|
1042
|
-
{noPresets && allItems.length === 0 && (
|
|
1043
|
-
<box flexDirection="column" paddingLeft={2} paddingRight={2}>
|
|
1044
|
-
<text fg={theme.colors.warning}>
|
|
1045
|
-
The style plugin is not installed.
|
|
1046
|
-
</text>
|
|
1047
|
-
<box marginTop={1}>
|
|
1048
|
-
<text fg={theme.colors.muted}>
|
|
1049
|
-
Presets come from style@magus. Install it from the
|
|
1050
|
-
</text>
|
|
1051
|
-
<text fg={theme.colors.muted}>
|
|
1052
|
-
Plugins tab, then press r to reload.
|
|
1053
|
-
</text>
|
|
1054
|
-
</box>
|
|
1055
|
-
<box marginTop={1}>
|
|
1056
|
-
<text fg={theme.colors.muted}>
|
|
1057
|
-
Output styles you have written yourself are still
|
|
1058
|
-
</text>
|
|
1059
|
-
<text fg={theme.colors.muted}>
|
|
1060
|
-
listed under Imported, with no plugin needed.
|
|
1061
|
-
</text>
|
|
1062
|
-
</box>
|
|
1063
|
-
</box>
|
|
1064
|
-
)}
|
|
1065
|
-
|
|
1066
1041
|
{allItems.length > 0 && (
|
|
1067
1042
|
<ScrollableList
|
|
1068
1043
|
items={allItems}
|
|
@@ -1077,8 +1052,7 @@ export function StylesScreen() {
|
|
|
1077
1052
|
|
|
1078
1053
|
{query.length > 0 &&
|
|
1079
1054
|
allItems.length === 0 &&
|
|
1080
|
-
stylesState.snapshot.status === "success" &&
|
|
1081
|
-
!noPresets && (
|
|
1055
|
+
stylesState.snapshot.status === "success" && (
|
|
1082
1056
|
<EmptyFilterState
|
|
1083
1057
|
query={stylesState.searchQuery}
|
|
1084
1058
|
entityName="styles"
|