Setting the file. One moment.
Extract Tag Endpoints · Clerk Backend API · clerk/skills · Skills Docs
ContentsBack to the top of the page (opens in a new tab)
scripts/ extract-tag-endpoints.sh
Shell · 208 lines · 6 KB
>
}
"
13 TMPDIR_WORK = $( mktemp -d )
14 trap 'rm -rf "$TMPDIR_WORK"' EXIT
15
16 SPEC = " $TMPDIR_WORK /spec.yml"
17 cat > " $SPEC "
18
19 # 1. Find all path+method blocks that have a matching tag
20 # Strategy: find line numbers of path entries (lines starting with " /"),
21 # then for each method block under that path, check if it contains the tag.
22
23 node - " $TAG " " $SPEC " << 'SCRIPT'
24 const fs = require("fs");
25 const tag = process.argv[2];
26 const specFile = process.argv[3];
27 const lines = fs.readFileSync(specFile, "utf8").split("\n");
28
29 const tagLower = tag.toLowerCase();
30
31 // Phase 1: Find all path definitions and their method blocks
32 // Paths start at indent 2 with " /"
33 // Methods start at indent 4 with " get:", " post:", etc.
34 const methods = ["get", "post", "put", "patch", "delete", "options", "head"];
35 const endpoints = [];
36 const refs = new Set();
37
38 let currentPath = null;
39 let currentMethod = null;
40 let blockStart = -1;
41 let blockLines = [];
42 let inPaths = false;
43 let inComponents = false;
44
45 // First pass: locate the "paths:" and "components:" top-level keys
46 let pathsStart = -1;
47 let pathsEnd = -1;
48 let componentsStart = -1;
49
50 for (let i = 0; i < lines.length; i++) {
51 const line = lines[i];
52 if (/^paths:\s*$/.test(line)) {
53 pathsStart = i;
54 } else if (pathsStart >= 0 && pathsEnd < 0 && /^\S/.test(line) && i > pathsStart) {
55 pathsEnd = i;
56 }
57 if (/^components:\s*$/.test(line)) {
58 componentsStart = i;
59 }
60 }
61 if (pathsEnd < 0) pathsEnd = lines.length;
62
63 // Second pass: extract endpoints matching the tag
64 function flushBlock() {
65 if (!currentPath || !currentMethod || blockLines.length === 0) return;
66
67 // Check if this block has the target tag
68 let inTags = false;
69 let hasTag = false;
70 const blockRefs = [];
71
72 for (const bl of blockLines) {
73 const trimmed = bl.trim();
74
75 // Detect tags section
76 if (/^tags:\s*$/.test(trimmed)) {
77 inTags = true;
78 continue;
79 }
80 if (inTags) {
81 if (/^- /.test(trimmed)) {
82 const tagVal = trimmed.replace(/^- /, "").trim().replace(/^['"]|['"]$/g, "");
83 if (tagVal.toLowerCase() === tagLower) hasTag = true;
84 } else {
85 inTags = false;
86 }
87 }
88
89 // Collect $ref values
90 const refMatch = bl.match(/\$ref:\s*['"]?(#\/[^'"}\s]+)['"]?/);
91 if (refMatch) blockRefs.push(refMatch[1]);
92 }
93
94 if (hasTag) {
95 // Extract summary, operationId, description
96 let summary = "";
97 let operationId = "";
98 let description = "";
99 let params = [];
100 let inDesc = false;
101 let inParams = false;
102
103 for (const bl of blockLines) {
104 const trimmed = bl.trim();
105 const indent = bl.length - bl.trimStart().length;
106
107 // Only capture operation-level keys (indent 6 = direct children of the method block)
108 if (indent === 6) {
109 const sumMatch = trimmed.match(/^summary:\s*(.+)/);
110 if (sumMatch) summary = sumMatch[1].replace(/^['"]|['"]$/g, "");
111
112 const opMatch = trimmed.match(/^operationId:\s*(.+)/);
113 if (opMatch) operationId = opMatch[1].replace(/^['"]|['"]$/g, "");
114
115 const descMatch = trimmed.match(/^description:\s*(.+)/);
116 if (descMatch && !inDesc) {
117 const val = descMatch[1].trim();
118 if (val === "|-" || val === "|" || val === ">-" || val === ">") {
119 inDesc = true;
120 } else {
121 description = val.replace(/^['"]|['"]$/g, "");
122 }
123 continue;
124 }
125 }
126
127 if (inDesc) {
128 // Continuation lines of description — grab first non-empty line
129 if (!description && trimmed.length > 0) {
130 description = trimmed;
131 }
132 // Stop when we hit the next operation-level key
133 if (indent === 6 && trimmed.length > 0 && !/^description:/.test(trimmed)) {
134 inDesc = false;
135 }
136 }
137 }
138
139 endpoints.push({
140 method: currentMethod.toUpperCase(),
141 path: currentPath,
142 operationId,
143 summary,
144 description,
145 refs: blockRefs,
146 });
147
148 for (const r of blockRefs) refs.add(r);
149 }
150 }
151
152 for (let i = pathsStart + 1; i < pathsEnd; i++) {
153 const line = lines[i];
154
155 // Path line: exactly 2 spaces + /
156 if (/^ {2}\/\S/.test(line)) {
157 flushBlock();
158 currentPath = line.trim().replace(/:$/, "");
159 currentMethod = null;
160 blockLines = [];
161 continue;
162 }
163
164 // Method line: exactly 4 spaces + method name
165 const methodMatch = line.match(/^ {4}(\w+):\s*$/);
166 if (methodMatch && methods.includes(methodMatch[1])) {
167 flushBlock();
168 currentMethod = methodMatch[1];
169 blockLines = [];
170 continue;
171 }
172
173 if (currentMethod) {
174 blockLines.push(line);
175 }
176 }
177 flushBlock();
178
179 // Output endpoints
180 if (endpoints.length === 0) {
181 console.error(`No endpoints found for tag: "${tag}"`);
182 process.exit(1);
183 }
184
185 console.log(`## Endpoints for "${tag}" (${endpoints.length} total)\n`);
186 for (const ep of endpoints) {
187 console.log(`### \`${ep.method}\` \`${ep.path}\``);
188 if (ep.operationId) console.log(`- **operationId**: \`${ep.operationId}\``);
189 if (ep.summary) console.log(`- **summary**: ${ep.summary}`);
190 if (ep.description && ep.description !== ep.summary)
191 console.log(`- **description**: ${ep.description}`);
192 if (ep.refs.length > 0) {
193 console.log(`- **refs**: ${ep.refs.map(r => "\`" + r.split("/").pop() + "\`").join(", ")}`);
194 }
195 console.log();
196 }
197
198 // Output unique refs list
199 if (refs.size > 0) {
200 console.log(`## Referenced Components (${refs.size} unique)\n`);
201 const sorted = [...refs].sort();
202 for (const r of sorted) {
203 const name = r.split("/").pop();
204 const category = r.split("/").slice(0, -1).join("/").replace("#/", "");
205 console.log(`- \`${name}\` (${category})`);
206 }
207 }
208 SCRIPT