dynamic config

This commit is contained in:
EthanShoeDev
2025-12-22 16:52:35 -05:00
parent ac51644bda
commit 3f18e881fa
14 changed files with 2077 additions and 89 deletions

View File

@@ -36,10 +36,7 @@
"name": "example-config",
"version": "0.0.1",
"dependencies": {
"@effect/cli": "catalog:",
"@effect/platform": "catalog:",
"@effect/platform-bun": "catalog:",
"effect": "catalog:",
"dsc-ts": "workspace:*",
},
"devDependencies": {
"typescript": "catalog:",

1814
docs/bun-bundler.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,22 @@
"type": "module",
"private": true,
"packageManager": "bun@1.3.3",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"winos-config": "./dist/bin.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"typecheck": "tsc --noEmit",
"build:lib": "tsc",
"build:bin": "bun build ./src/bin.ts --outdir ./dist --target bun --external \"*\" --minify",
"build": "bun run build:lib && bun run build:bin",
"gen:dsc-types": "bun run scripts/gen-dsc-types.ts",
"gen:dsc-resources-types": "bun run scripts/gen-dsc-resources-types.ts"
},

View File

@@ -1,16 +1,19 @@
#!/usr/bin/env bun
import { Command, Options } from '@effect/cli';
import { Command as PlatformCommand } from '@effect/platform';
import { FileSystem, Path, Command as PlatformCommand } from '@effect/platform';
import { BunContext, BunRuntime } from '@effect/platform-bun';
import { Effect } from 'effect';
import { Effect, Schema as S } from 'effect';
import pkg from '../package.json' with { type: 'json' };
import { type Configuration, ConfigurationSchema } from './dsl/dsl-core';
import {
decodeAndPrettyLogSetResult,
decodeAndPrettyLogTestResult,
} from './dsc-utils';
import { CommandUtils } from './utils';
} from './utils/dsc-utils';
import { CommandUtils } from './utils/utils';
const runDscConfig = (
subcommand: 'set' | 'test',
machineConfig: Configuration,
options: { whatIf?: boolean } = {},
) =>
Effect.gen(function* () {
@@ -54,7 +57,64 @@ const runDscConfig = (
}
});
const configPathOption = Options.file('configFile', { exists: 'yes' }).pipe(
const loadConfig = (configPath: string) =>
Effect.gen(function* () {
const path = yield* Path.Path;
const fs = yield* FileSystem.FileSystem;
const absolutePath = path.isAbsolute(configPath)
? configPath
: path.resolve(process.cwd(), configPath);
if (!(yield* fs.exists(absolutePath))) {
return yield* Effect.fail(
new Error(`Config file not found: ${absolutePath}`),
);
}
try {
// Bun can natively import .ts files
const module = yield* Effect.promise(() => import(absolutePath));
if (!module.default) {
return yield* Effect.fail(
new Error(
`Config file does not have a default export: ${absolutePath}`,
),
);
}
let config = module.default;
if (typeof config === 'function') {
config = yield* Effect.promise(() => Promise.resolve(config()));
}
// Flatten resources if they exist to support nested lists in the config
if (
config &&
typeof config === 'object' &&
'resources' in config &&
Array.isArray(config.resources)
) {
config = {
...config,
resources: (config.resources as Array<unknown>).flat(
Number.POSITIVE_INFINITY,
),
};
}
return yield* S.decodeUnknown(ConfigurationSchema)(config);
} catch (e) {
return yield* Effect.fail(
new Error(
`Failed to load config file: ${absolutePath}\n${e instanceof Error ? e.message : String(e)}`,
),
);
}
});
const configPathOption = Options.file('configFile').pipe(
Options.withDefault('winos-config.ts'),
);
@@ -65,8 +125,9 @@ const setCommand = Command.make(
},
({ configPathOption }) =>
Effect.gen(function* () {
const config = yield* loadConfig(configPathOption);
yield* Effect.log('Applying configuration changes...');
yield* runDscConfig('set').pipe(Effect.scoped);
yield* runDscConfig('set', config).pipe(Effect.scoped);
}),
);
@@ -77,8 +138,9 @@ const testCommand = Command.make(
},
({ configPathOption }) =>
Effect.gen(function* () {
const config = yield* loadConfig(configPathOption);
yield* Effect.log('Running configuration test...');
yield* runDscConfig('test').pipe(Effect.scoped);
yield* runDscConfig('test', config).pipe(Effect.scoped);
}),
);

View File

@@ -1,54 +0,0 @@
import * as S from 'effect/Schema';
import { ResourceUnion } from './dsc-resource-schema-types/_resource-union.gen';
import type * as Resources from './dsc-resource-schema-types/index';
import { Configuration as DscConfiguration } from './dsc-schema-types/configuration.gen';
/**
* Enhanced configuration schema with strong typing for resources.
* This extends the base DSC Configuration schema but overrides the 'resources'
* field with our strongly-typed union of all resources available on this system.
*/
export const Configuration = S.Struct({
...DscConfiguration.fields,
resources: S.Array(ResourceUnion),
});
export type Configuration = S.Schema.Type<typeof Configuration>;
/**
* Helper to define a configuration with full type safety.
* This provides a nice developer experience when defining the system state.
*
* @example
* const myConfig = defineConfig({
* $schema: 'https://aka.ms/dsc/schemas/v3/config/document.json',
* resources: [
* {
* type: 'Microsoft.WinGet/Package',
* name: 'Install VSCode',
* properties: {
* id: 'Microsoft.VisualStudioCode',
* ensure: 'Present'
* }
* }
* ]
* });
*/
export const defineConfig = (config: Configuration): Configuration => config;
export const defineWingetPackage = ({
id,
name,
...rest
}: {
id: string;
name?: string;
} & Partial<Resources.MicrosoftWinGetPackage.MicrosoftWinGetPackage>): Configuration['resources'][number] => ({
name: name ?? id,
type: 'Microsoft.WinGet/Package',
properties: {
id,
_exist: true,
...rest,
},
});

View File

@@ -0,0 +1,48 @@
import * as S from 'effect/Schema';
import { ResourceUnion } from '../dsc-resource-schema-types/_resource-union.gen';
import { Configuration as DscConfiguration } from '../dsc-schema-types/configuration.gen';
const defaultSchemaUri =
'https://aka.ms/dsc/schemas/v3/config/document.json' as const;
/**
* Enhanced configuration schema with strong typing for resources.
* This extends the base DSC Configuration schema but overrides the 'resources'
* field with our strongly-typed union of all resources available on this system.
*/
export const ConfigurationSchema = S.Struct({
...DscConfiguration.fields,
$schema: S.optional(DscConfiguration.fields.$schema).pipe(
S.withDefaults({
constructor: () => defaultSchemaUri,
decoding: () => defaultSchemaUri,
}),
),
resources: S.Array(ResourceUnion),
});
export type Configuration = S.Schema.Type<typeof ConfigurationSchema>;
/**
* A resource that can be nested in an array.
*/
export type UserResource =
| S.Schema.Encoded<typeof ResourceUnion>
| Array<UserResource>;
/**
* The configuration type that the user provides.
* This makes fields like $schema optional and allows nested resources.
*/
export interface UserConfiguration
extends Omit<S.Schema.Encoded<typeof ConfigurationSchema>, 'resources'> {
resources: Array<UserResource>;
}
/**
* The configuration can be a plain object or a function that returns the configuration.
* The function can be asynchronous.
*/
export type ConfigProvider =
| UserConfiguration
| (() => UserConfiguration | Promise<UserConfiguration>);

View File

@@ -0,0 +1,84 @@
import type * as Resources from '../dsc-resource-schema-types/index';
import type { ConfigProvider, Configuration } from './dsl-core';
/**
* Helper to define a configuration with full type safety.
* This provides a nice developer experience when defining the system state.
*
* @example
* const myConfig = defineConfig({
* resources: [
* defineWingetPackage({ id: 'Microsoft.VisualStudioCode' }),
* ]
* });
*
* @example
* const myConfig = defineConfig(async () => {
* return {
* resources: [
* defineWingetPackage({ id: 'Microsoft.VisualStudioCode' }),
* ]
* };
* });
*/
export const defineConfig = (config: ConfigProvider): ConfigProvider => config;
/**
* Helper to define a winget package resource.
*/
export const wingetPackage = ({
id,
name,
...rest
}: {
id: string;
name?: string;
} & Partial<Resources.MicrosoftWinGetPackage.MicrosoftWinGetPackage>): Configuration['resources'][number] => ({
name: name ?? id,
type: 'Microsoft.WinGet/Package',
properties: {
id,
_exist: true,
...rest,
},
});
/**
* Helper to define a registry key resource.
*/
export const registryKey = ({
keyPath,
name,
...rest
}: {
keyPath: string;
name?: string;
} & Partial<Resources.MicrosoftWindowsRegistry.MicrosoftWindowsRegistry>): Configuration['resources'][number] => ({
name: name ?? keyPath,
type: 'Microsoft.Windows/Registry',
properties: {
keyPath,
_exist: true,
...rest,
},
});
/**
* Helper to enable or disable dark mode for apps and system.
*/
export const darkMode = ({ enabled = true }: { enabled?: boolean }) => [
registryKey({
name: 'Enable Dark Mode (Apps)',
keyPath:
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize',
valueName: 'AppsUseLightTheme',
valueData: { DWord: enabled ? 0 : 1 },
}),
registryKey({
name: 'Enable Dark Mode (System)',
keyPath:
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize',
valueName: 'SystemUsesLightTheme',
valueData: { DWord: enabled ? 0 : 1 },
}),
];

View File

@@ -0,0 +1 @@
export * from './dsl/dsl-interface';

View File

@@ -1,8 +1,8 @@
import { Console, Effect, Schema as S } from 'effect';
import type { ResourceSetResponse } from './dsc-schema-types/configuration-set-result.gen';
import { ConfigurationSetResult as ConfigurationSetResultSchema } from './dsc-schema-types/configuration-set-result.gen';
import type { ResourceTestResponse } from './dsc-schema-types/configuration-test-result.gen';
import { ConfigurationTestResult as ConfigurationTestResultSchema } from './dsc-schema-types/configuration-test-result.gen';
import type { ResourceSetResponse } from '../dsc-schema-types/configuration-set-result.gen';
import { ConfigurationSetResult as ConfigurationSetResultSchema } from '../dsc-schema-types/configuration-set-result.gen';
import type { ResourceTestResponse } from '../dsc-schema-types/configuration-test-result.gen';
import { ConfigurationTestResult as ConfigurationTestResultSchema } from '../dsc-schema-types/configuration-test-result.gen';
/**
* Decodes and pretty-logs the result of a DSC configuration test operation.

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"noEmit": false,
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": false
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["winos-config.ts"],
"exclude": ["node_modules"]
}

View File

@@ -1,22 +1,22 @@
import { defineConfig, defineWingetPackage } from 'dsc-ts';
import { defineConfig, wingetPackage } from 'dsc-ts';
const machineConfig = defineConfig({
$schema: 'https://aka.ms/dsc/schemas/v3/config/document.json',
resources: [
// {
// name: 'example-registry-key',
// type: 'Microsoft.Windows/Registry',
// properties: {
// keyPath: 'HKCU\\Software\\WinosConfig',
// valueName: 'Version',
// valueData: {
// String: pkg.version,
// },
// _exist: true,
// },
// },
defineWingetPackage({ id: 'BurntSushi.ripgrep.MSVC' }),
],
export default defineConfig(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
resources: [
// {
// name: 'example-registry-key',
// type: 'Microsoft.Windows/Registry',
// properties: {
// keyPath: 'HKCU\\Software\\WinosConfig',
// valueName: 'Version',
// valueData: {
// String: pkg.version,
// },
// _exist: true,
// },
// },
wingetPackage({ id: 'BurntSushi.ripgrep.MSVC' }),
],
};
});
export default machineConfig;

View File

@@ -9,6 +9,10 @@
"with": ["//#lint:biome:check", "typecheck"]
},
"typecheck": {},
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"//#lint:biome": {},
"//#lint:biome:check": {}