@claude-code-mastery/starter-kit 1.0.0 → 1.2.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.
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Content Builder — Markdown Processor
3
+ *
4
+ * Converts markdown text to HTML with support for code blocks,
5
+ * tables, lists, blockquotes, headings, and inline formatting.
6
+ */
7
+
8
+ import type { CollectedHeading } from './types.js';
9
+
10
+ export function escapeHtml(text: string): string {
11
+ return text
12
+ .replace(/&/g, '&')
13
+ .replace(/</g, '&lt;')
14
+ .replace(/>/g, '&gt;');
15
+ }
16
+
17
+ export function slugify(text: string): string {
18
+ return text
19
+ .toLowerCase()
20
+ .replace(/[^\w\s-]/g, '')
21
+ .replace(/\s+/g, '-')
22
+ .replace(/-+/g, '-')
23
+ .replace(/^-|-$/g, '');
24
+ }
25
+
26
+ export function stripMarkdown(text: string): string {
27
+ return text
28
+ .replace(/\*\*\*(.*?)\*\*\*/g, '$1')
29
+ .replace(/\*\*(.*?)\*\*/g, '$1')
30
+ .replace(/\*(.*?)\*/g, '$1')
31
+ .replace(/`([^`]+)`/g, '$1')
32
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
33
+ }
34
+
35
+ export function processInlineFormatting(text: string): string {
36
+ let result = text;
37
+ result = result.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>');
38
+ result = result.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
39
+ result = result.replace(/\*(.*?)\*/g, '<em>$1</em>');
40
+ result = result.replace(/`([^`]+)`/g, '<code>$1</code>');
41
+ result = result.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
42
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
43
+ result = result.replace(/ {2}$/gm, '<br>');
44
+ return result;
45
+ }
46
+
47
+ function convertTableToHtml(rows: string[]): string {
48
+ if (rows.length === 0) return '';
49
+ let html = '<div class="table-wrapper">\n<table>\n';
50
+
51
+ rows.forEach((row, idx) => {
52
+ const cells = row.split('|').slice(1, -1);
53
+ const tag = idx === 0 ? 'th' : 'td';
54
+ html += '<tr>';
55
+ for (const cell of cells) {
56
+ let content = cell.trim();
57
+ content = content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
58
+ content = content.replace(/\*(.*?)\*/g, '<em>$1</em>');
59
+ html += `<${tag}>${content}</${tag}>`;
60
+ }
61
+ html += '</tr>\n';
62
+ });
63
+
64
+ html += '</table>\n</div>';
65
+ return html;
66
+ }
67
+
68
+ function processMarkdownTables(html: string): string {
69
+ const lines = html.split('\n');
70
+ const result: string[] = [];
71
+ let inTable = false;
72
+ let tableRows: string[] = [];
73
+
74
+ for (const line of lines) {
75
+ const trimmed = line.trim();
76
+
77
+ if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
78
+ if (!inTable) {
79
+ inTable = true;
80
+ tableRows = [];
81
+ }
82
+ if (!/^\|[\s\-:|]+\|$/.test(trimmed)) {
83
+ tableRows.push(trimmed);
84
+ }
85
+ } else {
86
+ if (inTable) {
87
+ result.push(convertTableToHtml(tableRows));
88
+ inTable = false;
89
+ tableRows = [];
90
+ }
91
+ result.push(line);
92
+ }
93
+ }
94
+
95
+ if (inTable && tableRows.length > 0) {
96
+ result.push(convertTableToHtml(tableRows));
97
+ }
98
+
99
+ return result.join('\n');
100
+ }
101
+
102
+ function processLists(html: string): string {
103
+ const lines = html.split('\n');
104
+ const result: string[] = [];
105
+ let inList = false;
106
+
107
+ for (const line of lines) {
108
+ const match = line.match(/^(\s*)- (.*)$/);
109
+ if (match) {
110
+ const content = processInlineFormatting(match[2]);
111
+ if (!inList) {
112
+ result.push('<ul>');
113
+ inList = true;
114
+ }
115
+ result.push(`<li>${content}</li>`);
116
+ } else {
117
+ if (inList) {
118
+ result.push('</ul>');
119
+ inList = false;
120
+ }
121
+ result.push(line);
122
+ }
123
+ }
124
+
125
+ if (inList) result.push('</ul>');
126
+ return result.join('\n');
127
+ }
128
+
129
+ function processOrderedLists(html: string): string {
130
+ const lines = html.split('\n');
131
+ const result: string[] = [];
132
+ let inList = false;
133
+
134
+ for (const line of lines) {
135
+ const match = line.match(/^\d+\.\s+(.*)$/);
136
+ if (match) {
137
+ if (!inList) {
138
+ result.push('<ol>');
139
+ inList = true;
140
+ }
141
+ result.push(`<li>${processInlineFormatting(match[1])}</li>`);
142
+ } else {
143
+ if (inList) {
144
+ result.push('</ol>');
145
+ inList = false;
146
+ }
147
+ result.push(line);
148
+ }
149
+ }
150
+
151
+ if (inList) result.push('</ol>');
152
+ return result.join('\n');
153
+ }
154
+
155
+ export function convertMarkdownToHtml(md: string, headings: CollectedHeading[]): string {
156
+ let html = md;
157
+
158
+ // Code blocks first (preserve content)
159
+ const codeBlocks: string[] = [];
160
+ html = html.replace(/````(\w*)\n([\s\S]*?)````/g, (_match, lang: string, code: string) => {
161
+ const placeholder = `___CODEBLOCK_${codeBlocks.length}___`;
162
+ codeBlocks.push(`<pre><code class="language-${lang || 'plaintext'}">${escapeHtml(code.trim())}</code></pre>`);
163
+ return placeholder;
164
+ });
165
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang: string, code: string) => {
166
+ const placeholder = `___CODEBLOCK_${codeBlocks.length}___`;
167
+ codeBlocks.push(`<pre><code class="language-${lang || 'plaintext'}">${escapeHtml(code.trim())}</code></pre>`);
168
+ return placeholder;
169
+ });
170
+
171
+ // Tables
172
+ html = processMarkdownTables(html);
173
+
174
+ // Blockquotes
175
+ html = html.replace(/^>\s*(.*)$/gm, '<blockquote><p>$1</p></blockquote>');
176
+ html = html.replace(/<\/blockquote>\n<blockquote>/g, '\n');
177
+
178
+ // Headings (collect for sidebar TOC)
179
+ html = html.replace(/^#### (.*)$/gm, (_m, text: string) => {
180
+ const id = slugify(text);
181
+ headings.push({ level: 4, id, text: stripMarkdown(text) });
182
+ return `<h4 id="${id}">${processInlineFormatting(text)}</h4>`;
183
+ });
184
+ html = html.replace(/^### (.*)$/gm, (_m, text: string) => {
185
+ const id = slugify(text);
186
+ headings.push({ level: 3, id, text: stripMarkdown(text) });
187
+ return `<h3 id="${id}">${processInlineFormatting(text)}</h3>`;
188
+ });
189
+ html = html.replace(/^## (.*)$/gm, (_m, text: string) => {
190
+ const id = slugify(text);
191
+ headings.push({ level: 2, id, text: stripMarkdown(text) });
192
+ return `<h2 id="${id}">${processInlineFormatting(text)}</h2>`;
193
+ });
194
+ html = html.replace(/^# (.*)$/gm, (_m, text: string) => {
195
+ const id = slugify(text);
196
+ headings.push({ level: 1, id, text: stripMarkdown(text) });
197
+ return `<h1 id="${id}">${processInlineFormatting(text)}</h1>`;
198
+ });
199
+
200
+ // Horizontal rules
201
+ html = html.replace(/^---$/gm, '<hr>');
202
+
203
+ // Lists
204
+ html = processLists(html);
205
+ html = processOrderedLists(html);
206
+
207
+ // Remaining inline formatting
208
+ html = html.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>');
209
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
210
+ html = html.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
211
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
212
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
213
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
214
+
215
+ // Paragraphs
216
+ const blockTags = /^<(h[1-6]|ul|ol|li|blockquote|pre|div|table|tr|th|td|hr|nav|header|footer|article|section)/i;
217
+ const rawLines = html.split('\n');
218
+ const groups: Array<{ type: string; lines?: string[]; content?: string }> = [];
219
+ let currentGroup: string[] = [];
220
+
221
+ for (const line of rawLines) {
222
+ const trimmed = line.trim();
223
+ if (trimmed === '' || blockTags.test(trimmed) || trimmed.startsWith('___CODEBLOCK_')) {
224
+ if (currentGroup.length > 0) {
225
+ groups.push({ type: 'paragraph', lines: currentGroup });
226
+ currentGroup = [];
227
+ }
228
+ if (trimmed !== '') {
229
+ groups.push({ type: 'html', content: trimmed });
230
+ }
231
+ } else if (line.endsWith(' ')) {
232
+ currentGroup.push(trimmed + '<br>');
233
+ } else {
234
+ currentGroup.push(trimmed);
235
+ }
236
+ }
237
+ if (currentGroup.length > 0) {
238
+ groups.push({ type: 'paragraph', lines: currentGroup });
239
+ }
240
+
241
+ const result: string[] = [];
242
+ for (const group of groups) {
243
+ if (group.type === 'html') {
244
+ result.push(group.content!);
245
+ } else if (group.type === 'paragraph' && group.lines) {
246
+ result.push(`<p>${group.lines.join('\n')}</p>`);
247
+ }
248
+ }
249
+
250
+ html = result.join('\n');
251
+
252
+ // Restore code blocks
253
+ codeBlocks.forEach((block, i) => {
254
+ html = html.replace(`___CODEBLOCK_${i}___`, block);
255
+ });
256
+
257
+ html = html.replace(/<p><\/p>/g, '');
258
+ html = html.replace(/\n{3,}/g, '\n\n');
259
+
260
+ return html;
261
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Content Builder — SEO & Schema.org Generator
3
+ *
4
+ * Generates JSON-LD structured data for articles and FAQ pages.
5
+ */
6
+
7
+ import type { ArticleConfig, ContentConfig } from './types.js';
8
+
9
+ export function generateSchemaJson(article: ArticleConfig, config: ContentConfig): string {
10
+ const schema: Record<string, unknown> = {
11
+ '@context': 'https://schema.org',
12
+ '@type': 'Article',
13
+ headline: article.title,
14
+ description: article.description,
15
+ url: article.url,
16
+ datePublished: article.datePublished,
17
+ dateModified: article.datePublished,
18
+ author: { '@type': 'Person', name: config.author, url: config.siteUrl },
19
+ publisher: { '@type': 'Person', name: config.author },
20
+ keywords: article.keywords ?? [],
21
+ isPartOf: article.parent
22
+ ? { '@type': 'Article', url: `${config.siteUrl}${article.parent.url}` }
23
+ : { '@type': 'WebSite', name: config.siteName, url: config.siteUrl },
24
+ };
25
+
26
+ let block = ` <script type="application/ld+json">\n ${JSON.stringify(schema, null, 4)}\n </script>`;
27
+
28
+ if (article.faqSchema && article.faqSchema.length > 0) {
29
+ const faqSchema = {
30
+ '@context': 'https://schema.org',
31
+ '@type': 'FAQPage',
32
+ mainEntity: article.faqSchema.map((faq) => ({
33
+ '@type': 'Question',
34
+ name: faq.question,
35
+ acceptedAnswer: { '@type': 'Answer', text: faq.answer },
36
+ })),
37
+ };
38
+ block += `\n <script type="application/ld+json">\n ${JSON.stringify(faqSchema, null, 4)}\n </script>`;
39
+ }
40
+
41
+ return block;
42
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Content Builder — Sidebar Generator
3
+ *
4
+ * Generates sidebar HTML with TOC, parent/child navigation, and grouping.
5
+ */
6
+
7
+ import type { ArticleConfig, CollectedHeading } from './types.js';
8
+
9
+ export function generateSidebarHtml(article: ArticleConfig, headings: CollectedHeading[]): string {
10
+ if (!article.sidebar) return '';
11
+
12
+ const parts: string[] = [];
13
+
14
+ if (article.parent) {
15
+ parts.push(` <a href="${article.parent.url}" class="sidebar-back">${article.parent.title}</a>`);
16
+ }
17
+
18
+ const maxLevel = article.tocLevel ?? (article.parent ? 2 : 1);
19
+ let tocHeadings = headings.filter((h) => h.level <= maxLevel);
20
+ if (article.tocFilter) {
21
+ const re = new RegExp(article.tocFilter);
22
+ tocHeadings = tocHeadings.filter((h) => re.test(h.text));
23
+ }
24
+
25
+ if (tocHeadings.length > 0) {
26
+ parts.push(' <div class="sidebar-label">CONTENTS</div>');
27
+ parts.push(' <ul class="sidebar-toc">');
28
+ for (const h of tocHeadings) {
29
+ const cls = h.level === 1 ? 'toc-h1' : 'toc-h2';
30
+ parts.push(` <li><a href="#${h.id}" class="${cls}">${h.text}</a></li>`);
31
+ }
32
+ parts.push(' </ul>');
33
+ }
34
+
35
+ if (article.children && article.children.length > 0) {
36
+ const currentPath = '/' + article.htmlOutput.replace(/index\.html$/, '');
37
+ const hasGroups = article.children.some((c) => c.group);
38
+
39
+ if (hasGroups) {
40
+ let listOpen = false;
41
+ for (const child of article.children) {
42
+ if (child.group) {
43
+ if (listOpen) parts.push(' </ul>');
44
+ parts.push(` <div class="sidebar-label">${child.group}</div>`);
45
+ parts.push(' <ul class="sidebar-links">');
46
+ listOpen = true;
47
+ }
48
+ const isCurrent = child.url === currentPath;
49
+ const cls = isCurrent ? ' class="sidebar-current"' : '';
50
+ parts.push(` <li><a href="${child.url}"${cls}>${child.title}</a></li>`);
51
+ }
52
+ if (listOpen) parts.push(' </ul>');
53
+ } else {
54
+ const label = article.childrenLabel ?? (article.parent ? 'RELATED' : 'DEEP DIVES');
55
+ parts.push(` <div class="sidebar-label">${label}</div>`);
56
+ parts.push(' <ul class="sidebar-links">');
57
+ for (const child of article.children) {
58
+ const isCurrent = child.url === currentPath;
59
+ const cls = isCurrent ? ' class="sidebar-current"' : '';
60
+ parts.push(` <li><a href="${child.url}"${cls}>${child.title}</a></li>`);
61
+ }
62
+ parts.push(' </ul>');
63
+ }
64
+ }
65
+
66
+ return ` <aside class="article-sidebar" id="articleSidebar">
67
+ <div class="sidebar-inner">
68
+ ${parts.join('\n')}
69
+ </div>
70
+ </aside>`;
71
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Content Builder — Types & Validation
3
+ *
4
+ * Shared types and Zod schemas for the content build system.
5
+ */
6
+
7
+ import { z } from 'zod';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Zod Schemas (runtime validation)
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const SidebarChildSchema = z.object({
14
+ title: z.string().min(1),
15
+ url: z.string().min(1),
16
+ group: z.string().optional(),
17
+ });
18
+
19
+ const FaqItemSchema = z.object({
20
+ question: z.string().min(1),
21
+ answer: z.string().min(1),
22
+ });
23
+
24
+ const ArticleConfigSchema = z.object({
25
+ id: z.string().min(1, 'Article id is required'),
26
+ published: z.boolean(),
27
+ mdSource: z.string().min(1, 'mdSource path is required'),
28
+ htmlOutput: z.string().min(1, 'htmlOutput path is required'),
29
+ title: z.string().min(1, 'title is required'),
30
+ titleHtml: z.string().optional(),
31
+ subtitle: z.string().optional(),
32
+ description: z.string().min(1, 'description is required'),
33
+ bannerImage: z.string().optional(),
34
+ bannerAlt: z.string().optional(),
35
+ url: z.string().url('url must be a valid URL'),
36
+ datePublished: z.string().min(1, 'datePublished is required'),
37
+ category: z.string().optional(),
38
+ tags: z.array(z.string()).optional(),
39
+ keywords: z.array(z.string()).optional(),
40
+ sidebar: z.boolean().optional(),
41
+ parent: z.object({ title: z.string(), url: z.string() }).optional(),
42
+ children: z.array(SidebarChildSchema).optional(),
43
+ childrenLabel: z.string().optional(),
44
+ tocLevel: z.number().int().min(1).max(6).optional(),
45
+ tocFilter: z.string().optional(),
46
+ faqSchema: z.array(FaqItemSchema).optional(),
47
+ });
48
+
49
+ const ContentConfigSchema = z.object({
50
+ siteUrl: z.string().url('siteUrl must be a valid URL'),
51
+ siteName: z.string().min(1, 'siteName is required'),
52
+ author: z.string().min(1, 'author is required'),
53
+ outputDir: z.string().min(1, 'outputDir is required'),
54
+ categories: z.array(z.string()),
55
+ articles: z.array(ArticleConfigSchema),
56
+ });
57
+
58
+ export { ContentConfigSchema, ArticleConfigSchema };
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // TypeScript interfaces (inferred from Zod for zero drift)
62
+ // ---------------------------------------------------------------------------
63
+
64
+ export type SidebarChild = z.infer<typeof SidebarChildSchema>;
65
+ export type ArticleConfig = z.infer<typeof ArticleConfigSchema>;
66
+ export type ContentConfig = z.infer<typeof ContentConfigSchema>;
67
+
68
+ export interface CollectedHeading {
69
+ level: number;
70
+ id: string;
71
+ text: string;
72
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "siteUrl": "https://example.com",
3
+ "siteName": "My Project",
4
+ "author": "Your Name",
5
+ "outputDir": "public/articles",
6
+ "categories": ["All", "Guides", "Updates", "Tutorials"],
7
+ "articles": [
8
+ {
9
+ "id": "getting-started",
10
+ "published": true,
11
+ "mdSource": "content/getting-started.md",
12
+ "htmlOutput": "public/articles/getting-started/index.html",
13
+ "title": "Getting Started Guide",
14
+ "titleHtml": "Getting <span>Started</span>",
15
+ "subtitle": "Everything you need to know",
16
+ "description": "A comprehensive guide to getting started with the project. Covers installation, configuration, and first steps.",
17
+ "bannerImage": "/images/getting-started-banner.webp",
18
+ "bannerAlt": "Getting Started Guide",
19
+ "url": "https://example.com/articles/getting-started/",
20
+ "datePublished": "2026-01-15",
21
+ "category": "Guides",
22
+ "tags": ["Guide", "Setup", "2026"],
23
+ "keywords": ["getting started", "setup", "installation", "tutorial"]
24
+ },
25
+ {
26
+ "id": "example-with-sidebar",
27
+ "published": false,
28
+ "mdSource": "content/advanced-guide.md",
29
+ "htmlOutput": "public/articles/advanced-guide/index.html",
30
+ "title": "Advanced Configuration",
31
+ "titleHtml": "Advanced <span>Configuration</span>",
32
+ "subtitle": "Deep dive into settings",
33
+ "description": "Advanced configuration options and patterns for power users.",
34
+ "bannerImage": "/images/advanced-banner.webp",
35
+ "bannerAlt": "Advanced Configuration Guide",
36
+ "url": "https://example.com/articles/advanced-guide/",
37
+ "datePublished": "2026-02-01",
38
+ "category": "Tutorials",
39
+ "tags": ["Advanced", "Configuration", "2026"],
40
+ "keywords": ["advanced", "configuration", "settings", "customization"],
41
+ "sidebar": true,
42
+ "children": [
43
+ { "title": "Database Setup", "url": "/articles/advanced-guide/database/" },
44
+ { "title": "Authentication", "url": "/articles/advanced-guide/auth/" },
45
+ { "title": "Deployment", "url": "/articles/advanced-guide/deploy/" }
46
+ ]
47
+ }
48
+ ]
49
+ }
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * db-query.ts — Test Query Master
4
+ *
5
+ * This is the MASTER INDEX for all database test/dev queries.
6
+ * It is NOT production code — it exists solely for developer exploration.
7
+ *
8
+ * HOW IT WORKS:
9
+ * - Each query lives in its own file under scripts/queries/
10
+ * - Every query file is registered here with a name and description
11
+ * - Run: npx tsx scripts/db-query.ts <query-name> [args...]
12
+ * - Run: npx tsx scripts/db-query.ts --list (to see all available queries)
13
+ *
14
+ * WHY THIS PATTERN:
15
+ * - Keeps test/dev database code COMPLETELY separate from production code
16
+ * - Every query receives a shared StrictDB instance — no raw driver imports
17
+ * - Easy to see at a glance what queries exist and what they do
18
+ * - Individual files are easy to review, modify, and delete
19
+ * - Production code in src/ never touches this — clean separation
20
+ *
21
+ * RULES:
22
+ * - EVERY query file MUST accept a StrictDB instance as its first parameter
23
+ * - NEVER import native database drivers directly in query files
24
+ * - NEVER copy query logic into production code — if you need it in prod,
25
+ * create a proper handler in src/handlers/
26
+ * - Each query file exports: { name, description, run(db, args) }
27
+ *
28
+ * Install: npm install tsx -D (if not already installed)
29
+ */
30
+
31
+ import { StrictDB } from 'strictdb';
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Query registry — add new queries here
35
+ // ---------------------------------------------------------------------------
36
+
37
+ interface QueryModule {
38
+ name: string;
39
+ description: string;
40
+ run: (db: StrictDB, args: string[]) => Promise<void>;
41
+ }
42
+
43
+ /**
44
+ * Register all query files here.
45
+ * Each entry maps a command name to its query module path.
46
+ *
47
+ * When Claude creates a new query for you, it will:
48
+ * 1. Create a new file in scripts/queries/<name>.ts
49
+ * 2. Add an entry to this registry
50
+ *
51
+ * Example:
52
+ * 'user-lookup': () => import('./queries/user-lookup.js'),
53
+ */
54
+ const queryRegistry: Record<string, () => Promise<{ default: QueryModule }>> = {
55
+ // --- Example queries (remove or modify as needed) ---
56
+ 'example-find-user': () => import('./queries/example-find-user.js'),
57
+ 'example-count-docs': () => import('./queries/example-count-docs.js'),
58
+
59
+ // --- Add your queries below this line ---
60
+ };
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // CLI runner
64
+ // ---------------------------------------------------------------------------
65
+
66
+ async function main(): Promise<void> {
67
+ const args = process.argv.slice(2);
68
+ const command = args[0];
69
+
70
+ // Show help
71
+ if (!command || command === '--help' || command === '-h') {
72
+ printUsage();
73
+ return;
74
+ }
75
+
76
+ // List all queries (no DB connection needed for listing)
77
+ if (command === '--list' || command === '-l') {
78
+ await listQueries();
79
+ return;
80
+ }
81
+
82
+ // Find and run the query
83
+ const loader = queryRegistry[command];
84
+ if (!loader) {
85
+ console.error(`\n Unknown query: "${command}"\n`);
86
+ console.error(' Run with --list to see available queries.\n');
87
+ process.exit(1);
88
+ }
89
+
90
+ const db = await StrictDB.create({ uri: process.env.STRICTDB_URI! });
91
+
92
+ try {
93
+ const mod = await loader();
94
+ const query = mod.default;
95
+ console.log(`\n Running: ${query.name}`);
96
+ console.log(` ${query.description}\n`);
97
+ await query.run(db, args.slice(1));
98
+ } catch (err) {
99
+ console.error('\n Query failed:', err);
100
+ process.exit(1);
101
+ } finally {
102
+ await db.close();
103
+ }
104
+ }
105
+
106
+ function printUsage(): void {
107
+ console.log(`
108
+ db-query — Test Query Master
109
+
110
+ Usage:
111
+ npx tsx scripts/db-query.ts <query-name> [args...]
112
+ npx tsx scripts/db-query.ts --list
113
+
114
+ Options:
115
+ --list, -l List all registered queries
116
+ --help, -h Show this help message
117
+
118
+ Examples:
119
+ npx tsx scripts/db-query.ts example-find-user test@example.com
120
+ npx tsx scripts/db-query.ts example-count-docs users
121
+ npx tsx scripts/db-query.ts --list
122
+ `);
123
+ }
124
+
125
+ async function listQueries(): Promise<void> {
126
+ console.log('\n Available queries:\n');
127
+
128
+ for (const [name, loader] of Object.entries(queryRegistry)) {
129
+ try {
130
+ const mod = await loader();
131
+ console.log(` ${name.padEnd(30)} ${mod.default.description}`);
132
+ } catch {
133
+ console.log(` ${name.padEnd(30)} (failed to load)`);
134
+ }
135
+ }
136
+
137
+ console.log('');
138
+ }
139
+
140
+ main().catch((err) => {
141
+ console.error('Fatal error:', err);
142
+ process.exit(1);
143
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Example query: Count documents in a collection
3
+ *
4
+ * Usage: npx tsx scripts/db-query.ts example-count-docs <collection>
5
+ *
6
+ * This is a TEST query — not production code.
7
+ */
8
+
9
+ import type { StrictDB } from 'strictdb';
10
+
11
+ export default {
12
+ name: 'example-count-docs',
13
+ description: 'Count documents in any collection',
14
+
15
+ async run(db: StrictDB, args: string[]): Promise<void> {
16
+ const collection = args[0];
17
+ if (!collection) {
18
+ console.error(' Usage: example-count-docs <collection>');
19
+ process.exit(1);
20
+ }
21
+
22
+ const total = await db.count(collection);
23
+ console.log(` Collection "${collection}" has ${total} documents.`);
24
+ },
25
+ };