import { Command, FileSystem, Path } from '@effect/platform'; import { BunContext, BunRuntime } from '@effect/platform-bun'; import { Effect, Logger, LogLevel, Schema as S } from 'effect'; import { jsonSchemaToEffectSchema } from './lib/script-utils'; // Schema for DSC resource list output const DscResourceSchema = S.Struct({ type: S.String, kind: S.String, version: S.String, capabilities: S.Array(S.String), path: S.String, description: S.NullOr(S.String), directory: S.String, manifest: S.Struct({ $schema: S.String, type: S.String, version: S.String, description: S.optional(S.NullOr(S.String)), schema: S.optional( S.NullOr( S.Union( S.Struct({ command: S.Struct({ executable: S.String, args: S.NullOr(S.Array(S.String)), }), }), S.Struct({ embedded: S.Unknown, }), ), ), ), }), }); type DscResource = S.Schema.Type; /** * Sanitize a resource type name to be a valid TypeScript identifier and filename * e.g., "Microsoft.WinGet/Package" -> "MicrosoftWinGetPackage" */ const sanitizeTypeName = (resourceType: string): string => { return resourceType .replace(/[/\\]/g, '') // Remove slashes .replace(/\./g, '') // Remove dots .replace(/[^a-zA-Z0-9]/g, ''); // Remove any other invalid chars }; /** * Convert resource type to a safe filename * e.g., "Microsoft.WinGet/Package" -> "microsoft-winget-package" */ const toSafeFilename = (resourceType: string): string => { return resourceType .replace(/[/\\]/g, '-') // Replace slashes with hyphens .replace(/\./g, '-') // Replace dots with hyphens .toLowerCase(); }; /** * Get schema for a DSC resource - either from embedded schema or via `dsc resource schema` */ const getDscResourceSchema = (resource: DscResource) => Effect.gen(function* () { const schema = resource.manifest.schema; if (!schema) { yield* Effect.logDebug(`No schema defined for ${resource.type}`); return null; } // Check if it's an embedded schema if ('embedded' in schema) { yield* Effect.logDebug(`Using embedded schema for ${resource.type}`); return JSON.stringify(schema.embedded); } // Otherwise, use dsc resource schema command (more reliable than running executable directly) if ('command' in schema) { yield* Effect.logDebug(`Getting schema via DSC CLI for ${resource.type}`); const schemaString = yield* Command.make( 'dsc', 'resource', 'schema', '--resource', resource.type, ).pipe(Command.string); return schemaString.trim(); } return null; }); /** * Parse NDJSON output (newline-delimited JSON) from dsc resource list */ const parseNdjson = (output: string): Array => output .split('\n') .map((line) => line.trim()) .filter((line) => line.length > 0) .map((line) => { try { return JSON.parse(line); } catch { return null; } }) .filter((obj): obj is unknown => obj !== null); /** * Get available DSC resources from the system */ const getAvailableDscResources = () => Effect.gen(function* () { yield* Effect.logDebug('Getting available DSC resources...'); const dscrResourcesListOutput = yield* Command.make( 'dsc', 'resource', 'list', '-o', 'json', ).pipe(Command.string); // Parse NDJSON output const rawResources = parseNdjson(dscrResourcesListOutput); yield* Effect.log(`Found ${rawResources.length} resources in DSC output`); // Decode each resource with the schema const resources: Array = []; for (const raw of rawResources) { const decoded = S.decodeUnknownOption(DscResourceSchema)(raw); if (decoded._tag === 'Some') { resources.push(decoded.value); } else { // Log which resource failed to parse for debugging const maybeType = typeof raw === 'object' && raw !== null && 'type' in raw ? (raw as { type: unknown }).type : 'unknown'; yield* Effect.logDebug(`Failed to decode resource: ${maybeType}`); } } yield* Effect.log(`Successfully decoded ${resources.length} resources`); return resources; }); /** * Generate index file that re-exports all types */ const generateIndexFile = ( resourceTypes: Array<{ type: string; filename: string; typeName: string }>, ) => { const lines = [ '// This file is auto-generated. Do not edit manually.', '// Re-exports all DSC resource schema types', '', ]; for (const { filename, typeName } of resourceTypes) { lines.push(`export * as ${typeName} from './${filename}';`); } lines.push(''); return lines.join('\n'); }; /** * Generate a types list file for all DSC resource configurations */ const generateDscConfigTypes = ( resourceTypes: Array<{ type: string; filename: string; typeName: string }>, ) => { const lines = [ '// This file is auto-generated. Do not edit manually.', "import * as S from 'effect/Schema';", '', ]; // Create a resource types literal lines.push('/**'); lines.push(' * All available DSC resource types on this system'); lines.push(' */'); lines.push('export const DscResourceTypes = S.Literal('); for (const { type } of resourceTypes) { lines.push(` "${type}",`); } lines.push(');'); lines.push(''); lines.push( 'export type DscResourceType = S.Schema.Type;', ); lines.push(''); // Create a map of type to schema import lines.push('/**'); lines.push(' * Map of resource type to generated file'); lines.push(' */'); lines.push('export const ResourceTypeToFile = {'); for (const { type, filename } of resourceTypes) { lines.push(` "${type}": "${filename}.gen",`); } lines.push('} as const;'); lines.push(''); return lines.join('\n'); }; const genDscResourcesTypes = Effect.gen(function* () { yield* Effect.log('Starting DSC resources types generation...'); const resources = yield* getAvailableDscResources(); // Filter to only resources that have schemas and are not adapters // (adapters like PowerShell adapter don't have their own config schema) const resourcesWithSchemas = resources.filter( (r) => r.manifest.schema && r.kind !== 'adapter', ); yield* Effect.log( `Found ${resourcesWithSchemas.length} resources with schemas (excluding adapters)`, ); const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const outputDir = path.join( process.cwd(), 'src', 'dsc-resource-schema-types', ); yield* Effect.logDebug(`Output directory: ${outputDir}`); if (yield* fs.exists(outputDir)) { yield* Effect.log('Removing existing output directory...'); yield* fs.remove(outputDir, { recursive: true, force: true }); } yield* fs.makeDirectory(outputDir, { recursive: true }); const generatedTypes: Array<{ type: string; filename: string; typeName: string; }> = []; const errors: Array<{ type: string; error: string }> = []; for (const resource of resourcesWithSchemas) { yield* Effect.logDebug(`Processing: ${resource.type}`); const schemaResult = yield* Effect.either(getDscResourceSchema(resource)); if (schemaResult._tag === 'Left') { errors.push({ type: resource.type, error: String(schemaResult.left), }); yield* Effect.logWarning( `Failed to get schema for ${resource.type}: ${schemaResult.left}`, ); continue; } const jsonSchema = schemaResult.right; if (!jsonSchema) { yield* Effect.logDebug(`No schema available for ${resource.type}`); continue; } const typeName = sanitizeTypeName(resource.type); const filename = toSafeFilename(resource.type); const effectSchemaResult = yield* Effect.either( jsonSchemaToEffectSchema({ jsonSchema, name: typeName, }), ); if (effectSchemaResult._tag === 'Left') { errors.push({ type: resource.type, error: String(effectSchemaResult.left), }); yield* Effect.logWarning( `Failed to convert schema for ${resource.type}: ${effectSchemaResult.left}`, ); continue; } const effectSchema = effectSchemaResult.right; const outputPath = path.join(outputDir, `${filename}.gen.ts`); yield* fs.writeFileString(outputPath, effectSchema); generatedTypes.push({ type: resource.type, filename, typeName, }); yield* Effect.log(`Generated: ${filename}.gen.ts (${resource.type})`); } // Generate index file if (generatedTypes.length > 0) { const indexContent = generateIndexFile(generatedTypes); const indexPath = path.join(outputDir, 'index.ts'); yield* fs.writeFileString(indexPath, indexContent); yield* Effect.log('Generated: index.ts'); // Generate types list file const typesListContent = generateDscConfigTypes(generatedTypes); const typesListPath = path.join(outputDir, '_resource-types.gen.ts'); yield* fs.writeFileString(typesListPath, typesListContent); yield* Effect.log('Generated: _resource-types.gen.ts'); } yield* Effect.log(''); yield* Effect.log('=== Generation Summary ==='); yield* Effect.log(`Successfully generated: ${generatedTypes.length} types`); if (errors.length > 0) { yield* Effect.log(`Failed: ${errors.length} types`); for (const { type, error } of errors) { yield* Effect.logDebug(` - ${type}: ${error}`); } } yield* Effect.log('DSC types generation complete!'); }); BunRuntime.runMain( genDscResourcesTypes.pipe( Effect.provide(BunContext.layer), Logger.withMinimumLogLevel(LogLevel.Debug), Effect.scoped, ), );