takomi 2.0.5 → 2.0.7
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 +35 -27
- package/assets/.agent/skills/21st-dev-components/21st-handoff.md +146 -0
- package/assets/.agent/skills/21st-dev-components/SKILL.md +198 -0
- package/assets/.agent/skills/21st-dev-components/references/categories.md +91 -0
- package/assets/.agent/skills/21st-dev-components/references/manual-handoff-template.md +79 -0
- package/assets/.agent/skills/21st-dev-components/references/section-detection-rubric.md +59 -0
- package/assets/.agent/skills/21st-dev-components/scripts/_shared.mjs +304 -0
- package/assets/.agent/skills/21st-dev-components/scripts/build-manual-handoff-template.mjs +115 -0
- package/assets/.agent/skills/21st-dev-components/scripts/fetch-21st-source.mjs +65 -0
- package/assets/.agent/skills/21st-dev-components/scripts/resolve-21st-component.mjs +115 -0
- package/assets/.agent/skills/code-review/SKILL.md +34 -81
- package/assets/.agent/skills/jstar-reviewer/SKILL.md +229 -0
- package/assets/.agent/skills/jstar-reviewer/agents/openai.yaml +7 -0
- package/assets/.agent/skills/takomi/workflows/review_code.md +96 -133
- package/assets/.agent/skills/takomi/workflows/spawn-jstar-code-review.md +161 -121
- package/assets/.agent/workflows/review_code.md +96 -133
- package/assets/.agent/workflows/spawn-jstar-code-review.md +161 -121
- package/package.json +7 -3
- package/src/cli.js +7 -7
- package/src/harness.js +13 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
export const CATEGORY_INDEX = {
|
|
4
|
+
announcement: { label: "Announcements", count: 10, url: "https://21st.dev/s/announcement" },
|
|
5
|
+
background: { label: "Backgrounds", count: 33, url: "https://21st.dev/s/background" },
|
|
6
|
+
border: { label: "Borders", count: 12, url: "https://21st.dev/s/border" },
|
|
7
|
+
"call-to-action": { label: "Calls to Action", count: 34, url: "https://21st.dev/s/call-to-action" },
|
|
8
|
+
comparison: { label: "Comparisons", count: 6, url: "https://21st.dev/s/comparison" },
|
|
9
|
+
dock: { label: "Docks", count: 6, url: "https://21st.dev/s/dock" },
|
|
10
|
+
features: { label: "Features", count: 36, url: "https://21st.dev/s/features" },
|
|
11
|
+
footer: { label: "Footers", count: 14, url: "https://21st.dev/s/footer" },
|
|
12
|
+
hero: { label: "Heroes", count: 73, url: "https://21st.dev/s/hero" },
|
|
13
|
+
hook: { label: "Hooks", count: 31, url: "https://21st.dev/s/hook" },
|
|
14
|
+
image: { label: "Images", count: 26, url: "https://21st.dev/s/image" },
|
|
15
|
+
map: { label: "Maps", count: 2, url: "https://21st.dev/s/map" },
|
|
16
|
+
"navbar-navigation": { label: "Navigation Menus", count: 11, url: "https://21st.dev/s/navbar-navigation" },
|
|
17
|
+
"pricing-section": { label: "Pricing Sections", count: 17, url: "https://21st.dev/s/pricing-section" },
|
|
18
|
+
"scroll-area": { label: "Scroll Areas", count: 24, url: "https://21st.dev/s/scroll-area" },
|
|
19
|
+
shader: { label: "Shaders", count: 15, url: "https://21st.dev/s/shader" },
|
|
20
|
+
testimonials: { label: "Testimonials", count: 15, url: "https://21st.dev/s/testimonials" },
|
|
21
|
+
text: { label: "Texts", count: 58, url: "https://21st.dev/s/text" },
|
|
22
|
+
video: { label: "Videos", count: 9, url: "https://21st.dev/s/video" },
|
|
23
|
+
accordion: { label: "Accordions", count: 40, url: "https://21st.dev/s/accordion" },
|
|
24
|
+
"ai-chat": { label: "AI Chats", count: 30, url: "https://21st.dev/s/ai-chat" },
|
|
25
|
+
alert: { label: "Alerts", count: 23, url: "https://21st.dev/s/alert" },
|
|
26
|
+
avatar: { label: "Avatars", count: 17, url: "https://21st.dev/s/avatar" },
|
|
27
|
+
badge: { label: "Badges", count: 25, url: "https://21st.dev/s/badge" },
|
|
28
|
+
button: { label: "Buttons", count: 130, url: "https://21st.dev/s/button" },
|
|
29
|
+
calendar: { label: "Calendars", count: 34, url: "https://21st.dev/s/calendar" },
|
|
30
|
+
card: { label: "Cards", count: 79, url: "https://21st.dev/s/card" },
|
|
31
|
+
carousel: { label: "Carousels", count: 16, url: "https://21st.dev/s/carousel" },
|
|
32
|
+
checkbox: { label: "Checkboxes", count: 19, url: "https://21st.dev/s/checkbox" },
|
|
33
|
+
"date-picker": { label: "Date Pickers", count: 12, url: "https://21st.dev/s/date-picker" },
|
|
34
|
+
"modal-dialog": { label: "Dialogs / Modals", count: 37, url: "https://21st.dev/s/modal-dialog" },
|
|
35
|
+
dropdown: { label: "Dropdowns", count: 25, url: "https://21st.dev/s/dropdown" },
|
|
36
|
+
"empty-state": { label: "Empty States", count: 1, url: "https://21st.dev/s/empty-state" },
|
|
37
|
+
"file-tree": { label: "File Trees", count: 2, url: "https://21st.dev/s/file-tree" },
|
|
38
|
+
"upload-download": { label: "File Uploads", count: 7, url: "https://21st.dev/s/upload-download" },
|
|
39
|
+
form: { label: "Forms", count: 23, url: "https://21st.dev/s/form" },
|
|
40
|
+
icons: { label: "Icons", count: 10, url: "https://21st.dev/s/icons" },
|
|
41
|
+
input: { label: "Inputs", count: 102, url: "https://21st.dev/s/input" },
|
|
42
|
+
link: { label: "Links", count: 13, url: "https://21st.dev/s/link" },
|
|
43
|
+
menu: { label: "Menus", count: 18, url: "https://21st.dev/s/menu" },
|
|
44
|
+
notification: { label: "Notifications", count: 5, url: "https://21st.dev/s/notification" },
|
|
45
|
+
number: { label: "Numbers", count: 18, url: "https://21st.dev/s/number" },
|
|
46
|
+
pagination: { label: "Paginations", count: 20, url: "https://21st.dev/s/pagination" },
|
|
47
|
+
popover: { label: "Popovers", count: 23, url: "https://21st.dev/s/popover" },
|
|
48
|
+
"radio-group": { label: "Radio Groups", count: 22, url: "https://21st.dev/s/radio-group" },
|
|
49
|
+
select: { label: "Selects", count: 62, url: "https://21st.dev/s/select" },
|
|
50
|
+
sidebar: { label: "Sidebars", count: 10, url: "https://21st.dev/s/sidebar" },
|
|
51
|
+
"sign-in": { label: "Sign Ins", count: 4, url: "https://21st.dev/s/sign-in" },
|
|
52
|
+
"registration-signup": { label: "Sign ups", count: 4, url: "https://21st.dev/s/registration-signup" },
|
|
53
|
+
slider: { label: "Sliders", count: 45, url: "https://21st.dev/s/slider" },
|
|
54
|
+
"spinner-loader": { label: "Spinner Loaders", count: 21, url: "https://21st.dev/s/spinner-loader" },
|
|
55
|
+
table: { label: "Tables", count: 30, url: "https://21st.dev/s/table" },
|
|
56
|
+
tabs: { label: "Tabs", count: 38, url: "https://21st.dev/s/tabs" },
|
|
57
|
+
"chip-tag": { label: "Tags", count: 6, url: "https://21st.dev/s/chip-tag" },
|
|
58
|
+
textarea: { label: "Text Areas", count: 22, url: "https://21st.dev/s/textarea" },
|
|
59
|
+
toast: { label: "Toasts", count: 2, url: "https://21st.dev/s/toast" },
|
|
60
|
+
toggle: { label: "Toggles", count: 12, url: "https://21st.dev/s/toggle" },
|
|
61
|
+
tooltip: { label: "Tooltips", count: 28, url: "https://21st.dev/s/tooltip" },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const SECTION_CATEGORY_MAP = {
|
|
65
|
+
announcement: ["announcement", "badge", "text", "link"],
|
|
66
|
+
header: ["announcement", "navbar-navigation", "button", "dropdown"],
|
|
67
|
+
navbar: ["navbar-navigation", "button", "dropdown", "menu"],
|
|
68
|
+
navigation: ["navbar-navigation", "sidebar", "dropdown", "menu"],
|
|
69
|
+
hero: ["hero", "call-to-action", "background", "text", "image"],
|
|
70
|
+
cta: ["call-to-action", "button", "badge", "text"],
|
|
71
|
+
features: ["features", "card", "icons", "number"],
|
|
72
|
+
logos: ["image", "icons", "scroll-area"],
|
|
73
|
+
testimonials: ["testimonials", "card", "carousel"],
|
|
74
|
+
pricing: ["pricing-section", "comparison", "card", "button"],
|
|
75
|
+
faq: ["accordion", "text"],
|
|
76
|
+
footer: ["footer", "link", "button"],
|
|
77
|
+
sidebar: ["sidebar", "menu", "navbar-navigation", "scroll-area"],
|
|
78
|
+
form: ["form", "input", "textarea", "select", "button"],
|
|
79
|
+
dashboard: ["sidebar", "table", "card", "tabs", "pagination"],
|
|
80
|
+
auth: ["sign-in", "registration-signup", "form", "input", "button"],
|
|
81
|
+
gallery: ["image", "carousel", "card", "video"],
|
|
82
|
+
blog: ["text", "card", "image", "pagination"],
|
|
83
|
+
stats: ["number", "card", "features"],
|
|
84
|
+
table: ["table", "pagination", "tabs"],
|
|
85
|
+
modal: ["modal-dialog", "popover", "button"],
|
|
86
|
+
chat: ["ai-chat", "input", "button", "avatar"],
|
|
87
|
+
map: ["map", "card", "popover"],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function parseArgs(argv) {
|
|
91
|
+
const parsed = { _: [] };
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
94
|
+
const arg = argv[i];
|
|
95
|
+
|
|
96
|
+
if (!arg.startsWith("--")) {
|
|
97
|
+
parsed._.push(arg);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
|
|
102
|
+
const key = rawKey.trim();
|
|
103
|
+
const nextValue = inlineValue ?? argv[i + 1];
|
|
104
|
+
const hasNextValue = inlineValue !== undefined || (nextValue && !nextValue.startsWith("--"));
|
|
105
|
+
|
|
106
|
+
if (!hasNextValue) {
|
|
107
|
+
parsed[key] = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const value = inlineValue ?? nextValue;
|
|
112
|
+
if (inlineValue === undefined) {
|
|
113
|
+
i += 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (parsed[key] === undefined) {
|
|
117
|
+
parsed[key] = value;
|
|
118
|
+
} else if (Array.isArray(parsed[key])) {
|
|
119
|
+
parsed[key].push(value);
|
|
120
|
+
} else {
|
|
121
|
+
parsed[key] = [parsed[key], value];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function forceArray(value) {
|
|
129
|
+
if (value === undefined) return [];
|
|
130
|
+
return Array.isArray(value) ? value : [value];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function fetchText(url) {
|
|
134
|
+
const response = await fetch(url, {
|
|
135
|
+
headers: {
|
|
136
|
+
"user-agent": "takomi-21st-dev-components/1.0",
|
|
137
|
+
accept: "text/html,application/json,text/plain,*/*",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`Request failed for ${url}: ${response.status} ${response.statusText}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return response.text();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function safeJsonParse(value) {
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(value);
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function slugifySectionName(section) {
|
|
157
|
+
return section
|
|
158
|
+
.toLowerCase()
|
|
159
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
160
|
+
.replace(/^-+|-+$/g, "");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function normalizeSections(sectionInput) {
|
|
164
|
+
if (!sectionInput) return [];
|
|
165
|
+
|
|
166
|
+
const trimmed = sectionInput.trim();
|
|
167
|
+
if (trimmed.startsWith("[")) {
|
|
168
|
+
const parsed = safeJsonParse(trimmed);
|
|
169
|
+
if (Array.isArray(parsed)) {
|
|
170
|
+
return parsed.map((item) => String(item).trim()).filter(Boolean);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return trimmed
|
|
175
|
+
.split(",")
|
|
176
|
+
.map((item) => item.trim())
|
|
177
|
+
.filter(Boolean);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getSectionCategories(section) {
|
|
181
|
+
const key = slugifySectionName(section);
|
|
182
|
+
return SECTION_CATEGORY_MAP[key] ?? [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function categoryDetails(slug) {
|
|
186
|
+
return CATEGORY_INDEX[slug] ?? { label: slug, count: null, url: `https://21st.dev/s/${slug}` };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function extractNextFlightPayloads(html) {
|
|
190
|
+
const marker = 'self.__next_f.push([1,"';
|
|
191
|
+
const payloads = [];
|
|
192
|
+
let cursor = 0;
|
|
193
|
+
|
|
194
|
+
while (true) {
|
|
195
|
+
const start = html.indexOf(marker, cursor);
|
|
196
|
+
if (start === -1) break;
|
|
197
|
+
|
|
198
|
+
let i = start + marker.length;
|
|
199
|
+
let raw = "";
|
|
200
|
+
|
|
201
|
+
while (i < html.length) {
|
|
202
|
+
if (html[i] === '"' && html.slice(i, i + 3) === '"])') {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (html[i] === "\\") {
|
|
207
|
+
raw += html.slice(i, i + 2);
|
|
208
|
+
i += 2;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
raw += html[i];
|
|
213
|
+
i += 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const decoded = safeJsonParse(`"${raw}"`);
|
|
217
|
+
if (typeof decoded === "string") {
|
|
218
|
+
payloads.push(decoded);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
cursor = i + 3;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return payloads;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function findJsonString(text, key) {
|
|
228
|
+
const pattern = new RegExp(`${escapeRegex(key)}"((?:[^"\\\\]|\\\\.)*)"`);
|
|
229
|
+
const match = pattern.exec(text);
|
|
230
|
+
if (!match) return null;
|
|
231
|
+
return safeJsonParse(`"${match[1]}"`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function findJsonObject(text, key, predicate = null) {
|
|
235
|
+
let cursor = 0;
|
|
236
|
+
|
|
237
|
+
while (true) {
|
|
238
|
+
const start = text.indexOf(key, cursor);
|
|
239
|
+
if (start === -1) return null;
|
|
240
|
+
|
|
241
|
+
const braceIndex = text.indexOf("{", start + key.length);
|
|
242
|
+
if (braceIndex === -1) return null;
|
|
243
|
+
|
|
244
|
+
const objectText = sliceBalancedJsonObject(text, braceIndex);
|
|
245
|
+
if (!objectText) return null;
|
|
246
|
+
|
|
247
|
+
const parsed = safeJsonParse(objectText);
|
|
248
|
+
if (parsed && (!predicate || predicate(parsed))) {
|
|
249
|
+
return parsed;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
cursor = braceIndex + 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function sliceBalancedJsonObject(text, startIndex) {
|
|
257
|
+
let depth = 0;
|
|
258
|
+
let inString = false;
|
|
259
|
+
let escaped = false;
|
|
260
|
+
|
|
261
|
+
for (let i = startIndex; i < text.length; i += 1) {
|
|
262
|
+
const char = text[i];
|
|
263
|
+
|
|
264
|
+
if (inString) {
|
|
265
|
+
if (escaped) {
|
|
266
|
+
escaped = false;
|
|
267
|
+
} else if (char === "\\") {
|
|
268
|
+
escaped = true;
|
|
269
|
+
} else if (char === '"') {
|
|
270
|
+
inString = false;
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (char === '"') {
|
|
276
|
+
inString = true;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (char === "{") {
|
|
281
|
+
depth += 1;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (char === "}") {
|
|
286
|
+
depth -= 1;
|
|
287
|
+
if (depth === 0) {
|
|
288
|
+
return text.slice(startIndex, i + 1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function deriveOutputPath(baseDir, sourceUrl) {
|
|
297
|
+
const url = new URL(sourceUrl);
|
|
298
|
+
const fileName = path.basename(url.pathname) || "source.txt";
|
|
299
|
+
return path.join(baseDir, fileName);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function escapeRegex(value) {
|
|
303
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
304
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import {
|
|
6
|
+
categoryDetails,
|
|
7
|
+
getSectionCategories,
|
|
8
|
+
normalizeSections,
|
|
9
|
+
parseArgs,
|
|
10
|
+
} from "./_shared.mjs";
|
|
11
|
+
|
|
12
|
+
function printUsage() {
|
|
13
|
+
console.log(`Usage:
|
|
14
|
+
node scripts/build-manual-handoff-template.mjs --sections hero,features,pricing [--reference-url <url>] [--goal <text>] [--out <file>]
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
node scripts/build-manual-handoff-template.mjs --sections hero,features,footer
|
|
18
|
+
node scripts/build-manual-handoff-template.mjs --sections '["navbar","hero","pricing"]' --reference-url https://example.com --out handoff.md
|
|
19
|
+
`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildTemplate({ sections, referenceUrl, goal }) {
|
|
23
|
+
const renderedSections = sections.map((section) => {
|
|
24
|
+
const categories = getSectionCategories(section);
|
|
25
|
+
const categoryLines = categories.length
|
|
26
|
+
? categories
|
|
27
|
+
.map((slug) => {
|
|
28
|
+
const details = categoryDetails(slug);
|
|
29
|
+
const countSuffix = details.count ? ` (${details.count})` : "";
|
|
30
|
+
return `- ${details.label}${countSuffix}: ${details.url}`;
|
|
31
|
+
})
|
|
32
|
+
.join("\n")
|
|
33
|
+
: "- Add any relevant 21st.dev category URLs you used.";
|
|
34
|
+
|
|
35
|
+
return `## ${section}
|
|
36
|
+
|
|
37
|
+
Suggested 21st.dev categories:
|
|
38
|
+
${categoryLines}
|
|
39
|
+
|
|
40
|
+
Chosen 21st.dev URLs:
|
|
41
|
+
-
|
|
42
|
+
|
|
43
|
+
Copy prompt or notes:
|
|
44
|
+
\`\`\`md
|
|
45
|
+
\`\`\`
|
|
46
|
+
|
|
47
|
+
Component code blocks:
|
|
48
|
+
\`\`\`tsx
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
51
|
+
Dependencies:
|
|
52
|
+
-
|
|
53
|
+
|
|
54
|
+
Placement / styling notes:
|
|
55
|
+
-
|
|
56
|
+
`;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return `# 21st.dev Manual Handoff
|
|
60
|
+
|
|
61
|
+
Reference URL:
|
|
62
|
+
${referenceUrl || ""}
|
|
63
|
+
|
|
64
|
+
Page goal:
|
|
65
|
+
${goal || ""}
|
|
66
|
+
|
|
67
|
+
Desired page order:
|
|
68
|
+
- ${sections.join("\n- ")}
|
|
69
|
+
|
|
70
|
+
General project notes:
|
|
71
|
+
- Framework:
|
|
72
|
+
- Styling stack:
|
|
73
|
+
- Existing component path:
|
|
74
|
+
- Constraints:
|
|
75
|
+
|
|
76
|
+
${renderedSections.join("\n")}
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function main() {
|
|
81
|
+
const args = parseArgs(process.argv.slice(2));
|
|
82
|
+
if (args.help || args.h) {
|
|
83
|
+
printUsage();
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sectionsInput = args.sections ?? args._[0];
|
|
88
|
+
const sections = normalizeSections(sectionsInput);
|
|
89
|
+
|
|
90
|
+
if (sections.length === 0) {
|
|
91
|
+
printUsage();
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const template = buildTemplate({
|
|
96
|
+
sections,
|
|
97
|
+
referenceUrl: args["reference-url"] ?? "",
|
|
98
|
+
goal: args.goal ?? "",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (args.out) {
|
|
102
|
+
const outputPath = path.resolve(process.cwd(), args.out);
|
|
103
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
104
|
+
await fs.writeFile(outputPath, template, "utf8");
|
|
105
|
+
console.log(outputPath);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
process.stdout.write(template);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
main().catch((error) => {
|
|
113
|
+
console.error(error.message);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { deriveOutputPath, fetchText, forceArray, parseArgs } from "./_shared.mjs";
|
|
6
|
+
|
|
7
|
+
function printUsage() {
|
|
8
|
+
console.log(`Usage:
|
|
9
|
+
node scripts/fetch-21st-source.mjs --url <cdn-url> [--url <cdn-url> ...] [--out-dir <dir>] [--json]
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
node scripts/fetch-21st-source.mjs --url https://cdn.21st.dev/user_Codehagen/hero-badge.tsx
|
|
13
|
+
node scripts/fetch-21st-source.mjs --url https://cdn.21st.dev/a.tsx --url https://cdn.21st.dev/b.tsx --out-dir tmp/21st
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
const args = parseArgs(process.argv.slice(2));
|
|
19
|
+
if (args.help || args.h) {
|
|
20
|
+
printUsage();
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const urls = [...forceArray(args.url), ...args._].filter(Boolean);
|
|
25
|
+
if (urls.length === 0) {
|
|
26
|
+
printUsage();
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const results = [];
|
|
31
|
+
for (const url of urls) {
|
|
32
|
+
const content = await fetchText(url);
|
|
33
|
+
results.push({
|
|
34
|
+
url,
|
|
35
|
+
fileName: path.basename(new URL(url).pathname) || "source.txt",
|
|
36
|
+
content,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (args["out-dir"]) {
|
|
41
|
+
const outDir = path.resolve(process.cwd(), args["out-dir"]);
|
|
42
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
43
|
+
|
|
44
|
+
for (const result of results) {
|
|
45
|
+
const outputPath = deriveOutputPath(outDir, result.url);
|
|
46
|
+
await fs.writeFile(outputPath, result.content, "utf8");
|
|
47
|
+
result.outputPath = outputPath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(JSON.stringify(results.map(({ content, ...rest }) => rest), null, 2));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (args.json || results.length > 1) {
|
|
55
|
+
console.log(JSON.stringify(results, null, 2));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
process.stdout.write(results[0].content);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main().catch((error) => {
|
|
63
|
+
console.error(error.message);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
extractNextFlightPayloads,
|
|
5
|
+
fetchText,
|
|
6
|
+
findJsonObject,
|
|
7
|
+
findJsonString,
|
|
8
|
+
parseArgs,
|
|
9
|
+
} from "./_shared.mjs";
|
|
10
|
+
|
|
11
|
+
function printUsage() {
|
|
12
|
+
console.log(`Usage:
|
|
13
|
+
node scripts/resolve-21st-component.mjs --url <21st-component-url> [--json]
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
node scripts/resolve-21st-component.mjs --url https://21st.dev/community/components/Codehagen/hero-badge/default --json
|
|
17
|
+
node scripts/resolve-21st-component.mjs --url https://21st.dev/community/components/Codehagen/hero-badge/default
|
|
18
|
+
`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeMetadata(url, html) {
|
|
22
|
+
const payloadText = extractNextFlightPayloads(html).join("\n");
|
|
23
|
+
const component = findJsonObject(payloadText, '"component":', (value) => typeof value?.component_slug === "string");
|
|
24
|
+
const demo = findJsonObject(payloadText, '"demo":', (value) => typeof value?.demo_slug === "string");
|
|
25
|
+
const registryDependencies =
|
|
26
|
+
findJsonObject(payloadText, '"registryDependencies":') ??
|
|
27
|
+
findJsonObject(payloadText, '"registry_dependencies":');
|
|
28
|
+
const npmDependenciesOfRegistryDependencies =
|
|
29
|
+
findJsonObject(payloadText, '"npmDependenciesOfRegistryDependencies":') ??
|
|
30
|
+
findJsonObject(payloadText, '"npm_dependencies_of_registry_dependencies":');
|
|
31
|
+
const tailwindConfig = findJsonObject(payloadText, '"tailwindConfig":');
|
|
32
|
+
const globalCss = findJsonString(payloadText, '"globalCss":"');
|
|
33
|
+
const demoCodeInline = findJsonString(payloadText, '"demoCode":"');
|
|
34
|
+
|
|
35
|
+
if (!component) {
|
|
36
|
+
throw new Error("Could not locate structured component metadata in the 21st.dev page payload.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
inputUrl: url,
|
|
41
|
+
fetchedAt: new Date().toISOString(),
|
|
42
|
+
title: component.name ?? component.component_slug,
|
|
43
|
+
componentName: component.name ?? null,
|
|
44
|
+
componentSlug: component.component_slug ?? null,
|
|
45
|
+
componentNames: Array.isArray(component.component_names) ? component.component_names : [],
|
|
46
|
+
author: component.user?.username ?? component.user?.name ?? component.user_id ?? null,
|
|
47
|
+
authorProfile: component.user?.website_url ?? component.user?.github_url ?? null,
|
|
48
|
+
description: component.description ?? null,
|
|
49
|
+
license: component.license ?? null,
|
|
50
|
+
tags: Array.isArray(component.tags) ? component.tags.map((tag) => tag.slug ?? tag.name).filter(Boolean) : [],
|
|
51
|
+
dependencies: component.dependencies ?? {},
|
|
52
|
+
demoDependencies: demo?.demo_dependencies ?? component.demo_dependencies ?? {},
|
|
53
|
+
demoDirectRegistryDependencies:
|
|
54
|
+
demo?.demo_direct_registry_dependencies ??
|
|
55
|
+
component.demo_direct_registry_dependencies ??
|
|
56
|
+
[],
|
|
57
|
+
directRegistryDependencies: component.direct_registry_dependencies ?? [],
|
|
58
|
+
registryDependencies: registryDependencies ?? {},
|
|
59
|
+
npmDependenciesOfRegistryDependencies: npmDependenciesOfRegistryDependencies ?? {},
|
|
60
|
+
codeUrl: component.code ?? null,
|
|
61
|
+
demoCodeUrl: demo?.demo_code ?? component.demo_code ?? null,
|
|
62
|
+
previewUrl: demo?.preview_url ?? component.preview_url ?? null,
|
|
63
|
+
videoUrl: demo?.video_url ?? component.video_url ?? null,
|
|
64
|
+
compiledCssUrl: demo?.compiled_css ?? component.compiled_css ?? null,
|
|
65
|
+
tailwindConfig,
|
|
66
|
+
globalCss,
|
|
67
|
+
demoCodeInline,
|
|
68
|
+
websiteUrl: component.website_url ?? null,
|
|
69
|
+
downloadsCount: component.downloads_count ?? null,
|
|
70
|
+
likesCount: component.likes_count ?? null,
|
|
71
|
+
demoName: demo?.name ?? null,
|
|
72
|
+
demoSlug: demo?.demo_slug ?? null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function main() {
|
|
77
|
+
const args = parseArgs(process.argv.slice(2));
|
|
78
|
+
if (args.help || args.h) {
|
|
79
|
+
printUsage();
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const url = args.url ?? args._[0];
|
|
84
|
+
if (!url) {
|
|
85
|
+
printUsage();
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const html = await fetchText(url);
|
|
90
|
+
const metadata = normalizeMetadata(url, html);
|
|
91
|
+
|
|
92
|
+
if (args.json) {
|
|
93
|
+
console.log(JSON.stringify(metadata, null, 2));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lines = [
|
|
98
|
+
`Title: ${metadata.title ?? "Unknown"}`,
|
|
99
|
+
`Author: ${metadata.author ?? "Unknown"}`,
|
|
100
|
+
`Component Slug: ${metadata.componentSlug ?? "Unknown"}`,
|
|
101
|
+
`Demo Slug: ${metadata.demoSlug ?? "Unknown"}`,
|
|
102
|
+
`Tags: ${metadata.tags.join(", ") || "None"}`,
|
|
103
|
+
`Dependencies: ${Object.keys(metadata.dependencies).join(", ") || "None"}`,
|
|
104
|
+
`Code URL: ${metadata.codeUrl ?? "None"}`,
|
|
105
|
+
`Demo Code URL: ${metadata.demoCodeUrl ?? "None"}`,
|
|
106
|
+
`Preview URL: ${metadata.previewUrl ?? "None"}`,
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
console.log(lines.join("\n"));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
main().catch((error) => {
|
|
113
|
+
console.error(error.message);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|