@rbleattler/omp-ts-typegen 0.2025.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,416 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { validate } from './validator';
4
+ import { exec } from 'child_process';
5
+ import { promisify } from 'util';
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ const TYPES_DIR = path.join(process.cwd(), 'src', 'types');
10
+ const REPO_CACHE_DIR = path.join(process.cwd(), 'cache', 'oh-my-posh');
11
+ const THEMES_DIR = path.join(REPO_CACHE_DIR, 'themes');
12
+ const REPO_URL = 'https://github.com/JanDeDobbeleer/oh-my-posh.git';
13
+ const VALIDATION_REPORT = path.join(process.cwd(), 'theme-validation.md');
14
+ const DETAILED_REPORT = path.join(process.cwd(), 'theme-validation-details.md');
15
+ const BADGE_PATH = path.join(process.cwd(), 'theme-validation-badge.svg');
16
+ const TEMP_DIR = path.join(process.cwd(), 'temp');
17
+
18
+ async function ensureRepoCloned(): Promise<void> {
19
+ try {
20
+ // Check if the repo directory exists
21
+ await fs.access(REPO_CACHE_DIR);
22
+
23
+ // If it exists, pull the latest changes
24
+ console.log('Oh My Posh repo exists, pulling latest changes...');
25
+ await execAsync('git pull', { cwd: REPO_CACHE_DIR });
26
+ } catch (error) {
27
+ // If the directory doesn't exist, clone the repo
28
+ console.log('Cloning Oh My Posh repository...');
29
+ await fs.mkdir(path.dirname(REPO_CACHE_DIR), { recursive: true });
30
+ await execAsync(`git clone ${REPO_URL} ${REPO_CACHE_DIR} --depth 1`);
31
+ }
32
+ }
33
+
34
+ async function getThemeFiles(): Promise<string[]> {
35
+ try {
36
+ // Get all files in the themes directory
37
+ const files = await fs.readdir(THEMES_DIR);
38
+
39
+ // Filter for JSON files only, excluding schema.json
40
+ const themeFiles = files
41
+ .filter(file => file.endsWith('.json') && file !== 'schema.json')
42
+ .map(file => path.join(THEMES_DIR, file));
43
+
44
+ return themeFiles;
45
+ } catch (error) {
46
+ throw new Error(`Failed to get theme files: ${error}`);
47
+ }
48
+ }
49
+
50
+ async function validateTheme(themePath: string): Promise<{
51
+ path: string,
52
+ name: string,
53
+ valid: boolean,
54
+ errors?: string[]
55
+ }> {
56
+ try {
57
+ const themeName = path.basename(themePath);
58
+ const content = await fs.readFile(themePath, 'utf8');
59
+ const config = JSON.parse(content);
60
+
61
+ // Pass the theme name to the validator
62
+ const result = await validate(config, themeName);
63
+
64
+ return {
65
+ path: themePath,
66
+ name: themeName,
67
+ valid: result.valid,
68
+ errors: result.errors
69
+ };
70
+ } catch (error: any) {
71
+ return {
72
+ path: themePath,
73
+ name: path.basename(themePath),
74
+ valid: false,
75
+ errors: [`Error processing theme: ${error.message}`]
76
+ };
77
+ }
78
+ }
79
+
80
+ async function generateMarkdownReport(results: {
81
+ total: number,
82
+ valid: number,
83
+ invalid: number,
84
+ validThemes: Array<{ name: string, path: string }>,
85
+ invalidThemes: Array<{ name: string, path: string, errors: string[] }>
86
+ }): Promise<void> {
87
+ const { default: chalk } = await import('chalk');
88
+ console.log(chalk.blue('Generating validation report...'));
89
+
90
+ const repoOwner = 'JanDeDobbeleer';
91
+ const repoName = 'oh-my-posh';
92
+ const branch = 'main'; // Assuming 'main' is the default branch
93
+
94
+ const reportHeader = `# Oh My Posh Theme Validation Report
95
+
96
+ This report shows the validation status of all themes in the [Oh My Posh](https://github.com/${repoOwner}/${repoName}) repository against the TypeScript types generated from the schema.
97
+
98
+ - **Total themes tested:** ${results.total}
99
+ - **Valid themes:** ${results.valid} (${Math.round((results.valid / results.total) * 100)}%)
100
+ - **Invalid themes:** ${results.invalid} (${Math.round((results.invalid / results.total) * 100)}%)
101
+
102
+ Last updated: ${new Date().toISOString().split('T')[0]}
103
+
104
+ `;
105
+
106
+ const tableHeader = `| Theme | Status |
107
+ |-------|--------|
108
+ `;
109
+
110
+ let tableRows = '';
111
+
112
+ // Add valid themes first
113
+ for (const theme of results.validThemes) {
114
+ const themeName = path.basename(theme.name, '.json');
115
+ const themeUrl = `https://github.com/${repoOwner}/${repoName}/blob/${branch}/themes/${theme.name}`;
116
+ tableRows += `| [${themeName}](${themeUrl}) | ✅ Valid |\n`;
117
+ }
118
+
119
+ // Then add invalid themes
120
+ for (const theme of results.invalidThemes) {
121
+ const themeName = path.basename(theme.name, '.json');
122
+ const themeUrl = `https://github.com/${repoOwner}/${repoName}/blob/${branch}/themes/${theme.name}`;
123
+ tableRows += `| [${themeName}](${themeUrl}) | ❌ Invalid |\n`;
124
+ }
125
+
126
+ const reportContent = reportHeader + tableHeader + tableRows;
127
+ await fs.writeFile(VALIDATION_REPORT, reportContent);
128
+ console.log(chalk.green(`Validation report generated at ${VALIDATION_REPORT}`));
129
+ }
130
+
131
+ /**
132
+ * Generates a detailed markdown report with all validation errors
133
+ */
134
+ async function generateDetailedReport(results: {
135
+ total: number,
136
+ valid: number,
137
+ invalid: number,
138
+ validThemes: Array<{ name: string, path: string }>,
139
+ invalidThemes: Array<{ name: string, path: string, errors: string[] }>
140
+ }): Promise<void> {
141
+ const { default: chalk } = await import('chalk');
142
+ console.log(chalk.blue('Generating detailed validation report...'));
143
+
144
+ const repoOwner = 'JanDeDobbeleer';
145
+ const repoName = 'oh-my-posh';
146
+ const branch = 'main';
147
+
148
+ const reportHeader = `# Oh My Posh Theme Validation - Detailed Error Report
149
+
150
+ This report contains detailed validation errors for each theme that failed validation.
151
+ These errors can help identify issues in either the theme files or in the TypeScript type definitions.
152
+
153
+ - **Total themes tested:** ${results.total}
154
+ - **Valid themes:** ${results.valid} (${Math.round((results.valid / results.total) * 100)}%)
155
+ - **Invalid themes:** ${results.invalid} (${Math.round((results.invalid / results.total) * 100)}%)
156
+
157
+ Last updated: ${new Date().toISOString().split('T')[0]}
158
+
159
+ ## Summary
160
+
161
+ ${results.invalid === 0 ?
162
+ '✅ All themes passed validation!' :
163
+ `⚠️ ${results.invalid} themes failed validation with type errors.`}
164
+
165
+ ${results.invalid > 0 ? '## Error Details\n\nThe following sections contain detailed error information for each invalid theme.\n' : ''}
166
+ `;
167
+
168
+ let detailContent = '';
169
+
170
+ // Add details for invalid themes
171
+ for (const theme of results.invalidThemes) {
172
+ const themeName = path.basename(theme.name, '.json');
173
+ const themeUrl = `https://github.com/${repoOwner}/${repoName}/blob/${branch}/themes/${theme.name}`;
174
+
175
+ detailContent += `### [${themeName}](${themeUrl})\n\n`;
176
+ detailContent += `**Validation Errors:**\n\n`;
177
+ detailContent += theme.errors?.map(error => `- ${error}`).join('\n') + '\n\n';
178
+ }
179
+
180
+ const reportContent = reportHeader + detailContent;
181
+ await fs.writeFile(DETAILED_REPORT, reportContent);
182
+ console.log(chalk.green(`Detailed validation report generated at ${DETAILED_REPORT}`));
183
+ }
184
+
185
+ /**
186
+ * Generates a custom SVG badge showing theme validation status
187
+ */
188
+ async function generateValidationBadge(results: {
189
+ total: number,
190
+ valid: number,
191
+ invalid: number
192
+ }): Promise<void> {
193
+ const { default: chalk } = await import('chalk');
194
+ console.log(chalk.blue('Generating validation badge SVG...'));
195
+
196
+ const percentage = Math.round((results.valid / results.total) * 100);
197
+ const width = 240;
198
+ const height = 20;
199
+ const labelWidth = 70;
200
+ const valueWidth = width - labelWidth;
201
+
202
+ // Determine color based on percentage
203
+ let color = '#e05d44'; // red
204
+ if (percentage > 80) color = '#4c1'; // bright green
205
+ else if (percentage > 60) color = '#97CA00'; // green
206
+ else if (percentage > 40) color = '#dfb317'; // yellow
207
+
208
+ const text = `${results.valid}/${results.total} themes valid (${percentage}%)`;
209
+
210
+ // Create SVG badge
211
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
212
+ <linearGradient id="b" x2="0" y2="100%">
213
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
214
+ <stop offset="1" stop-opacity=".1"/>
215
+ </linearGradient>
216
+ <mask id="a">
217
+ <rect width="${width}" height="${height}" rx="3" fill="#fff"/>
218
+ </mask>
219
+ <g mask="url(#a)">
220
+ <path fill="#555" d="M0 0h${labelWidth}v${height}H0z"/>
221
+ <path fill="${color}" d="M${labelWidth} 0h${valueWidth}v${height}H${labelWidth}z"/>
222
+ <path fill="url(#b)" d="M0 0h${width}v${height}H0z"/>
223
+ </g>
224
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
225
+ <text x="${labelWidth / 2}" y="15" fill="#010101" fill-opacity=".3">Themes</text>
226
+ <text x="${labelWidth / 2}" y="14">Themes</text>
227
+ <text x="${labelWidth + valueWidth / 2}" y="15" fill="#010101" fill-opacity=".3">${text}</text>
228
+ <text x="${labelWidth + valueWidth / 2}" y="14">${text}</text>
229
+ </g>
230
+ </svg>`;
231
+
232
+ await fs.writeFile(BADGE_PATH, svg);
233
+ console.log(chalk.green(`Validation badge generated at ${BADGE_PATH}`));
234
+ }
235
+
236
+ /**
237
+ * Updates the README to include the custom validation badge
238
+ */
239
+ async function updateReadmeBadge(): Promise<void> {
240
+ const { default: chalk } = await import('chalk');
241
+ const readmePath = path.join(process.cwd(), 'readme.md');
242
+
243
+ try {
244
+ // Read the README content
245
+ let readmeContent = await fs.readFile(readmePath, 'utf-8');
246
+
247
+ // Create badge markdown
248
+ const badgeMarkdown = `![Theme Validation](./theme-validation-badge.svg)`;
249
+
250
+ // Replace existing badge or add new one after the title
251
+ if (readmeContent.includes('![Theme Validation]')) {
252
+ readmeContent = readmeContent.replace(
253
+ /!\[Theme Validation\]\(.+?\)/,
254
+ badgeMarkdown
255
+ );
256
+ } else {
257
+ // Add after the first heading
258
+ readmeContent = readmeContent.replace(
259
+ /(# .+?\n)/,
260
+ `$1\n${badgeMarkdown}\n`
261
+ );
262
+ }
263
+
264
+ // Write updated README
265
+ await fs.writeFile(readmePath, readmeContent);
266
+ console.log(chalk.green('Updated README with custom validation badge'));
267
+ } catch (error) {
268
+ console.error(chalk.red('Failed to update README badge:'), error);
269
+ }
270
+ }
271
+
272
+ async function main() {
273
+ // Dynamically import chalk
274
+ const { default: chalk } = await import('chalk');
275
+
276
+ console.log(chalk.blue('Testing generated types against all Oh My Posh themes...'));
277
+
278
+ try {
279
+ // Check if the types file exists
280
+ const typesFile = path.join(TYPES_DIR, 'omp.ts');
281
+ await fs.access(typesFile);
282
+ console.log(chalk.green('Types file exists at:'), typesFile);
283
+
284
+ // Check if the OhMyPosh type is exported
285
+ const typesContent = await fs.readFile(typesFile, 'utf-8');
286
+ if (typesContent.includes('export interface OhMyPosh') ||
287
+ typesContent.includes('export type OhMyPosh')) {
288
+ console.log(chalk.green('OhMyPosh type is properly exported!'));
289
+ } else {
290
+ console.log(chalk.red('Warning: OhMyPosh type may not be properly exported!'));
291
+ console.log('Searching for OhMyPosh definition...');
292
+
293
+ // Find the OhMyPosh definition
294
+ const match = typesContent.match(/(?:interface|type)\s+OhMyPosh/);
295
+ if (match) {
296
+ console.log(chalk.yellow(`Found "${match[0]}" but it might not be exported properly`));
297
+ } else {
298
+ console.log(chalk.red('Could not find OhMyPosh type definition!'));
299
+ }
300
+ }
301
+
302
+ // First test with our bundled default config
303
+ // console.log(chalk.green('Testing with local default.omp.json...'));
304
+ // const defaultResult = await validate(defaultConfig);
305
+ // if (defaultResult.valid) {
306
+ // console.log(chalk.green('✅ Local default config is valid!'));
307
+ // } else {
308
+ // console.error(chalk.red('❌ Local default config validation failed:'));
309
+ // defaultResult.errors?.forEach(err => console.error(chalk.red(err)));
310
+ // // Don't exit early, continue with other themes
311
+ // }
312
+
313
+ // Clone or update the Oh My Posh repository
314
+ console.log(chalk.yellow('Ensuring Oh My Posh repository is up to date...'));
315
+ await ensureRepoCloned();
316
+
317
+ // Get all theme files
318
+ console.log(chalk.yellow('Finding themes in Oh My Posh repository...'));
319
+ const themeFiles = await getThemeFiles();
320
+ console.log(chalk.green(`Found ${themeFiles.length} themes to validate`));
321
+
322
+ // Track validation results
323
+ const results = {
324
+ total: themeFiles.length,
325
+ valid: 0,
326
+ invalid: 0,
327
+ validThemes: [] as Array<{ name: string, path: string }>,
328
+ invalidThemes: [] as Array<{ name: string, path: string, errors: string[] }>
329
+ };
330
+
331
+ // Process each theme
332
+ console.log(chalk.yellow('Validating Oh My Posh themes against generated types...'));
333
+
334
+ // Create a progress indicator
335
+ let completed = 0;
336
+
337
+ // Validate themes in batches to avoid overwhelming the system
338
+ const BATCH_SIZE = 5;
339
+ for (let i = 0; i < themeFiles.length; i += BATCH_SIZE) {
340
+ const batch = themeFiles.slice(i, i + BATCH_SIZE);
341
+ const batchResults = await Promise.all(batch.map(themePath => validateTheme(themePath)));
342
+
343
+ for (const result of batchResults) {
344
+ completed++;
345
+
346
+ if (result.valid) {
347
+ results.valid++;
348
+ results.validThemes.push({
349
+ name: path.basename(result.path),
350
+ path: result.path
351
+ });
352
+ process.stdout.write(chalk.green('.'));
353
+ } else {
354
+ results.invalid++;
355
+ results.invalidThemes.push({
356
+ name: path.basename(result.path),
357
+ path: result.path,
358
+ errors: result.errors || []
359
+ });
360
+ process.stdout.write(chalk.red('✗'));
361
+ }
362
+
363
+ // Print progress every 10 themes
364
+ if (completed % 10 === 0) {
365
+ process.stdout.write(` ${completed}/${themeFiles.length}\n`);
366
+ }
367
+ }
368
+ }
369
+
370
+ // Print final newline
371
+ process.stdout.write('\n');
372
+
373
+ // Generate the markdown report
374
+ await generateMarkdownReport(results);
375
+
376
+ // Generate detailed markdown report
377
+ await generateDetailedReport(results);
378
+
379
+ // Generate custom SVG badge and update README
380
+ await generateValidationBadge(results);
381
+ await updateReadmeBadge();
382
+
383
+ // Report results to console
384
+ console.log(chalk.blue('Theme validation complete:'));
385
+ console.log(chalk.green(`✅ Valid themes: ${results.valid}/${results.total}`));
386
+
387
+ if (results.invalid > 0) {
388
+ console.log(chalk.red(`❌ Invalid themes: ${results.invalid}/${results.total}`));
389
+ console.log(chalk.yellow('See theme-validation.md for detailed report'));
390
+ process.exit(1);
391
+ } else {
392
+ console.log(chalk.green('✅ All themes passed validation!'));
393
+ }
394
+
395
+ } catch (error) {
396
+ // Get chalk again in case error happens before first import
397
+ const { default: chalk } = await import('chalk');
398
+ console.error(chalk.red('Error testing types:'), error);
399
+ process.exit(1);
400
+ }
401
+ await fs.rm(TEMP_DIR, { recursive: true }).catch(() => { });
402
+ // Clean up all temporary files at the end
403
+ try {
404
+ console.log(chalk.yellow('Cleaning up temporary files...'));
405
+ await fs.rm(TEMP_DIR, { recursive: true }).catch(() => {});
406
+ console.log(chalk.green('Temporary files cleaned up!'));
407
+ } catch (error) {
408
+ console.log(chalk.yellow('Failed to clean up some temporary files.'));
409
+ }
410
+ }
411
+
412
+ main().catch(async (error) => {
413
+ const { default: chalk } = await import('chalk');
414
+ console.error(chalk.red('Unhandled error:'), error);
415
+ process.exit(1);
416
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Note: This is a simplified validator that checks if the JSON is
3
+ * compatible with the TypeScript interfaces. For a full check,
4
+ * we would need to compile the TypeScript and use the runtime types.
5
+ */
6
+
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import { exec } from 'child_process';
10
+ import { promisify } from 'util';
11
+
12
+ const execAsync = promisify(exec);
13
+
14
+ const TEMP_DIR = path.join(process.cwd(), 'temp');
15
+
16
+ /**
17
+ * Validates a JSON config against the TypeScript interface
18
+ * @param config The configuration object to validate
19
+ * @param themeName Optional theme name to use as part of the test file name
20
+ * @returns Validation result with valid status and any errors
21
+ */
22
+ export async function validate(config: any, themeName?: string): Promise<{ valid: boolean; errors?: string[] }> {
23
+ const { default: chalk } = await import('chalk');
24
+
25
+ // Create a unique filename based on the theme name or a timestamp
26
+ const safeThemeName = themeName ?
27
+ themeName.replace(/[^\w.-]/g, '_') :
28
+ `config_${Date.now()}`;
29
+
30
+ const testFilePath = path.join(TEMP_DIR, `${safeThemeName}.test.ts`);
31
+
32
+ try {
33
+ // Create temp directory if it doesn't exist
34
+ await fs.mkdir(TEMP_DIR, { recursive: true });
35
+
36
+ // Create a temporary TypeScript file that imports the types and validates the config
37
+ const testContent = `
38
+ // This is a generated test file for ${themeName || 'unknown theme'}
39
+ import { OhMyPosh } from '../src/types/omp';
40
+
41
+ // The config to validate
42
+ const config: OhMyPosh = ${JSON.stringify(config, null, 2)};
43
+
44
+ // If TypeScript compiles this, it means the config is valid
45
+ console.log('Config is valid!');
46
+ `;
47
+
48
+ // Save the test file
49
+ await fs.writeFile(testFilePath, testContent);
50
+
51
+ try {
52
+ // Run tsc with verbose error output and report all errors
53
+ const { stdout, stderr } = await execAsync(`npx tsc --noEmit --pretty ${testFilePath}`);
54
+
55
+ // If we get here and stderr is empty, compilation succeeded
56
+ if (!stderr.trim()) {
57
+ return { valid: true };
58
+ } else {
59
+ return {
60
+ valid: false,
61
+ errors: [
62
+ `TypeScript validation failed for ${themeName || 'config'}:`,
63
+ stderr
64
+ ]
65
+ };
66
+ }
67
+ } catch (execError: any) {
68
+ // TypeScript compilation failed - get the full error details
69
+ return {
70
+ valid: false,
71
+ errors: [
72
+ `TypeScript validation failed for ${themeName || 'config'}:`,
73
+ execError.stderr || execError.message || 'Unknown error'
74
+ ]
75
+ };
76
+ } finally {
77
+ // Clean up the test file
78
+ try {
79
+ await fs.unlink(testFilePath).catch(() => {});
80
+ } catch (error) {
81
+ // Ignore errors during cleanup
82
+ }
83
+ }
84
+ } catch (error: any) {
85
+ // Other errors (file system, etc.)
86
+ return { valid: false, errors: [`Error during validation: ${error.message}`] };
87
+ }
88
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './types/omp';
@@ -0,0 +1,20 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="240" height="20" viewBox="0 0 240 20">
2
+ <linearGradient id="b" x2="0" y2="100%">
3
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
4
+ <stop offset="1" stop-opacity=".1"/>
5
+ </linearGradient>
6
+ <mask id="a">
7
+ <rect width="240" height="20" rx="3" fill="#fff"/>
8
+ </mask>
9
+ <g mask="url(#a)">
10
+ <path fill="#555" d="M0 0h70v20H0z"/>
11
+ <path fill="#4c1" d="M70 0h170v20H70z"/>
12
+ <path fill="url(#b)" d="M0 0h240v20H0z"/>
13
+ </g>
14
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
15
+ <text x="35" y="15" fill="#010101" fill-opacity=".3">Themes</text>
16
+ <text x="35" y="14">Themes</text>
17
+ <text x="155" y="15" fill="#010101" fill-opacity=".3">122/122 themes valid (100%)</text>
18
+ <text x="155" y="14">122/122 themes valid (100%)</text>
19
+ </g>
20
+ </svg>