claudeup 4.37.0 → 4.38.1
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 +531 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudeup",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.38.1",
|
|
4
4
|
"description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.tsx",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"typescript": "^5.6.3"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
|
-
"claudeup-darwin-arm64": "4.
|
|
68
|
-
"claudeup-darwin-x64": "4.
|
|
69
|
-
"claudeup-linux-x64": "4.
|
|
67
|
+
"claudeup-darwin-arm64": "4.38.1",
|
|
68
|
+
"claudeup-darwin-x64": "4.38.1",
|
|
69
|
+
"claudeup-linux-x64": "4.38.1"
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* verify-community-registry.ts — the gate between a proposed community style
|
|
4
|
+
* entry and a committed one.
|
|
5
|
+
*
|
|
6
|
+
* This is NOT a test. It hits the real network, it is run by a human who is
|
|
7
|
+
* curating the registry, and it must never run in CI: it would burn the shared
|
|
8
|
+
* 60/hr `api.github.com` budget on every push and then fail on GitHub's rate
|
|
9
|
+
* limit rather than on our bug, which is the worst possible signal.
|
|
10
|
+
*
|
|
11
|
+
* Two modes:
|
|
12
|
+
*
|
|
13
|
+
* --discover list the real `.md` blobs and their upstream
|
|
14
|
+
* --discover <repo> <dir> `name`, for every registry source or for one
|
|
15
|
+
* candidate repo. Prints a paste-ready registry
|
|
16
|
+
* block. NEVER hand-write a path from a README:
|
|
17
|
+
* a wrong path is a 404 the user sees, and the
|
|
18
|
+
* error text blames claudeup for it, correctly.
|
|
19
|
+
*
|
|
20
|
+
* --check re-fetch every committed entry and report 404s,
|
|
21
|
+
* size outliers and unparseable files. Run this
|
|
22
|
+
* before committing a registry change.
|
|
23
|
+
*
|
|
24
|
+
* Cost: `--discover` spends ONE api.github.com call per repo (a recursive git
|
|
25
|
+
* tree), then reads names over raw.githubusercontent.com, which costs nothing.
|
|
26
|
+
* `--check` spends zero API budget.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { parseArgs } from "node:util";
|
|
30
|
+
import {
|
|
31
|
+
COMMUNITY_SOURCES,
|
|
32
|
+
COMMUNITY_STYLES,
|
|
33
|
+
type CommunityStyleSource,
|
|
34
|
+
coordinateProblem,
|
|
35
|
+
rawUrlFor,
|
|
36
|
+
} from "../src/data/community-styles.js";
|
|
37
|
+
import { githubFetcher } from "../src/services/community-fetcher.js";
|
|
38
|
+
import { MAX_STYLE_BYTES } from "../src/services/community-styles.js";
|
|
39
|
+
import { splitFrontmatter } from "../src/services/styles-manager.js";
|
|
40
|
+
|
|
41
|
+
interface DiscoveredFile {
|
|
42
|
+
path: string;
|
|
43
|
+
/** Upstream's own `name`, or the basename when the file declares none. */
|
|
44
|
+
displayName: string;
|
|
45
|
+
description: string;
|
|
46
|
+
bytes: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `attention-span/output-styles/zen-master.md` -> `zen-master`. */
|
|
50
|
+
function slugFromPath(filePath: string): string {
|
|
51
|
+
return (filePath.split("/").pop() ?? filePath).replace(/\.md$/, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function discover(
|
|
55
|
+
repo: string,
|
|
56
|
+
dir: string,
|
|
57
|
+
ref: string,
|
|
58
|
+
): Promise<DiscoveredFile[]> {
|
|
59
|
+
// ONE API call for the whole repo. Asking per-directory would cost one call
|
|
60
|
+
// per level and tell us less.
|
|
61
|
+
const treeUrl = `https://api.github.com/repos/${repo}/git/trees/${ref}?recursive=1`;
|
|
62
|
+
const tree = await githubFetcher(treeUrl, {
|
|
63
|
+
accept: "application/vnd.github+json",
|
|
64
|
+
});
|
|
65
|
+
if (tree.status !== 200) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${repo}: HTTP ${tree.status} listing the git tree${
|
|
68
|
+
tree.status === 403 || tree.status === 429
|
|
69
|
+
? " (rate limited — wait, or export GITHUB_TOKEN)"
|
|
70
|
+
: ""
|
|
71
|
+
}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const parsed = JSON.parse(tree.body) as {
|
|
75
|
+
truncated?: boolean;
|
|
76
|
+
tree?: Array<{ path?: string; type?: string }>;
|
|
77
|
+
};
|
|
78
|
+
if (parsed.truncated) {
|
|
79
|
+
console.warn(
|
|
80
|
+
` ! ${repo}: git tree was truncated by GitHub — list is partial`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const prefix = `${dir}/`;
|
|
85
|
+
const paths = (parsed.tree ?? [])
|
|
86
|
+
.filter(
|
|
87
|
+
(entry) =>
|
|
88
|
+
entry.type === "blob" &&
|
|
89
|
+
typeof entry.path === "string" &&
|
|
90
|
+
entry.path.startsWith(prefix) &&
|
|
91
|
+
entry.path.endsWith(".md") &&
|
|
92
|
+
// One directory deep only: the cache is flat and a nested style would
|
|
93
|
+
// need a slug that is not a plain basename.
|
|
94
|
+
!entry.path.slice(prefix.length).includes("/"),
|
|
95
|
+
)
|
|
96
|
+
.map((entry) => entry.path as string)
|
|
97
|
+
.sort();
|
|
98
|
+
|
|
99
|
+
const files: DiscoveredFile[] = [];
|
|
100
|
+
for (const filePath of paths) {
|
|
101
|
+
// raw.githubusercontent.com costs no API budget, so reading every file to
|
|
102
|
+
// get its real `name` is free. This is the only way to get a displayName
|
|
103
|
+
// that is upstream's own rather than one we invented from a filename.
|
|
104
|
+
const raw = await githubFetcher(
|
|
105
|
+
`https://raw.githubusercontent.com/${repo}/${ref}/${filePath}`,
|
|
106
|
+
);
|
|
107
|
+
if (raw.status !== 200) {
|
|
108
|
+
console.warn(` ! ${filePath}: HTTP ${raw.status}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const { frontmatter } = splitFrontmatter(raw.body);
|
|
112
|
+
files.push({
|
|
113
|
+
path: filePath,
|
|
114
|
+
displayName: frontmatter.name || slugFromPath(filePath),
|
|
115
|
+
description: frontmatter.description || "",
|
|
116
|
+
bytes: Buffer.byteLength(raw.body, "utf8"),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return files;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function printDiscovered(
|
|
123
|
+
sourceId: string,
|
|
124
|
+
repo: string,
|
|
125
|
+
files: DiscoveredFile[],
|
|
126
|
+
): void {
|
|
127
|
+
console.log(`\n${repo} — ${files.length} style file(s)`);
|
|
128
|
+
console.log("─".repeat(72));
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const flag = file.bytes > MAX_STYLE_BYTES ? " ** OVER 64 KiB **" : "";
|
|
131
|
+
console.log(
|
|
132
|
+
` {\n\t\tid: "${sourceId}--${slugFromPath(file.path)}",\n\t\tsourceId: "${sourceId}",\n\t\tpath: "${file.path}",\n\t\tdisplayName: ${JSON.stringify(file.displayName)},\n\t\tsummary: ${JSON.stringify(file.description.slice(0, 100))},\n\t},${flag}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function runDiscover(repo?: string, dir?: string): Promise<number> {
|
|
138
|
+
const targets: Array<{ id: string; repo: string; dir: string; ref: string }> =
|
|
139
|
+
repo && dir
|
|
140
|
+
? [{ id: repo.split("/")[1] ?? repo, repo, dir, ref: "HEAD" }]
|
|
141
|
+
: COMMUNITY_SOURCES.map((source: CommunityStyleSource) => ({
|
|
142
|
+
id: source.id,
|
|
143
|
+
repo: source.repo,
|
|
144
|
+
dir: source.dir,
|
|
145
|
+
ref: source.ref,
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
let failures = 0;
|
|
149
|
+
for (const target of targets) {
|
|
150
|
+
try {
|
|
151
|
+
const files = await discover(target.repo, target.dir, target.ref);
|
|
152
|
+
printDiscovered(target.id, target.repo, files);
|
|
153
|
+
if (files.length === 0) {
|
|
154
|
+
// The rule that keeps `caveman` and `humanizer` out: zero style files
|
|
155
|
+
// means there is nothing to import, whatever the star count says.
|
|
156
|
+
console.log(
|
|
157
|
+
" (no style files — this repo must NOT get a registry entry)",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
failures++;
|
|
162
|
+
console.error(
|
|
163
|
+
` ! ${target.repo}: ${error instanceof Error ? error.message : String(error)}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return failures;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function runCheck(): Promise<number> {
|
|
171
|
+
let problems = 0;
|
|
172
|
+
console.log(
|
|
173
|
+
`Checking ${COMMUNITY_STYLES.length} entries across ${COMMUNITY_SOURCES.length} sources — zero API budget.\n`,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
for (const style of COMMUNITY_STYLES) {
|
|
177
|
+
const source = COMMUNITY_SOURCES.find((s) => s.id === style.sourceId);
|
|
178
|
+
if (!source) {
|
|
179
|
+
console.error(
|
|
180
|
+
`✗ ${style.id}: sourceId "${style.sourceId}" resolves to nothing`,
|
|
181
|
+
);
|
|
182
|
+
problems++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const coordinate = coordinateProblem(style, source);
|
|
186
|
+
if (coordinate) {
|
|
187
|
+
console.error(`✗ ${style.id}: ${coordinate}`);
|
|
188
|
+
problems++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const response = await githubFetcher(rawUrlFor(style, source));
|
|
193
|
+
if (response.status !== 200) {
|
|
194
|
+
console.error(
|
|
195
|
+
`✗ ${style.id}: HTTP ${response.status} — ${rawUrlFor(style, source)}`,
|
|
196
|
+
);
|
|
197
|
+
problems++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const bytes = Buffer.byteLength(response.body, "utf8");
|
|
201
|
+
if (bytes > MAX_STYLE_BYTES) {
|
|
202
|
+
console.error(`✗ ${style.id}: ${bytes} bytes exceeds the 64 KiB cap`);
|
|
203
|
+
problems++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const { frontmatter, body } = splitFrontmatter(response.body);
|
|
207
|
+
if (Object.keys(frontmatter).length === 0 || body.trim().length === 0) {
|
|
208
|
+
console.error(
|
|
209
|
+
`✗ ${style.id}: not a valid output style (no frontmatter or no body)`,
|
|
210
|
+
);
|
|
211
|
+
problems++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Advisory, not a failure: upstream is free to rename a style, and the
|
|
215
|
+
// coordinate id is deliberately independent of it. But a drifted
|
|
216
|
+
// displayName is what the user reads, so it is worth seeing.
|
|
217
|
+
const upstreamName = frontmatter.name;
|
|
218
|
+
const drift =
|
|
219
|
+
upstreamName && upstreamName !== style.displayName
|
|
220
|
+
? ` (upstream name is now "${upstreamName}", registry says "${style.displayName}")`
|
|
221
|
+
: "";
|
|
222
|
+
console.log(
|
|
223
|
+
`✓ ${style.id.padEnd(38)} ${String(bytes).padStart(6)} B${drift}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(
|
|
228
|
+
problems === 0
|
|
229
|
+
? "\nRegistry is clean — safe to commit."
|
|
230
|
+
: `\n${problems} problem(s). Do NOT commit until they are fixed.`,
|
|
231
|
+
);
|
|
232
|
+
return problems;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function main(): Promise<number> {
|
|
236
|
+
const { values, positionals } = parseArgs({
|
|
237
|
+
args: process.argv.slice(2),
|
|
238
|
+
options: {
|
|
239
|
+
discover: { type: "boolean", default: false },
|
|
240
|
+
check: { type: "boolean", default: false },
|
|
241
|
+
},
|
|
242
|
+
allowPositionals: true,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (!values.discover && !values.check) {
|
|
246
|
+
console.error(
|
|
247
|
+
"usage: bun scripts/verify-community-registry.ts --discover [<repo> <dir>]\n" +
|
|
248
|
+
" bun scripts/verify-community-registry.ts --check",
|
|
249
|
+
);
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let problems = 0;
|
|
254
|
+
if (values.discover) {
|
|
255
|
+
problems += await runDiscover(positionals[0], positionals[1]);
|
|
256
|
+
}
|
|
257
|
+
if (values.check) {
|
|
258
|
+
problems += await runCheck();
|
|
259
|
+
}
|
|
260
|
+
return problems === 0 ? 0 : 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (import.meta.main) {
|
|
264
|
+
main()
|
|
265
|
+
.then((code) => process.exit(code))
|
|
266
|
+
.catch((error) => {
|
|
267
|
+
console.error(
|
|
268
|
+
`error: ${error instanceof Error ? error.message : String(error)}`,
|
|
269
|
+
);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|
|
272
|
+
}
|