dynamic config
This commit is contained in:
5
bun.lock
5
bun.lock
@@ -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
1814
docs/bun-bundler.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
48
packages/dsc-ts/src/dsl/dsl-core.ts
Normal file
48
packages/dsc-ts/src/dsl/dsl-core.ts
Normal 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>);
|
||||
84
packages/dsc-ts/src/dsl/dsl-interface.ts
Normal file
84
packages/dsc-ts/src/dsl/dsl-interface.ts
Normal 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 },
|
||||
}),
|
||||
];
|
||||
1
packages/dsc-ts/src/index.ts
Normal file
1
packages/dsc-ts/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dsl/dsl-interface';
|
||||
@@ -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.
|
||||
13
packages/dsc-ts/tsconfig.json
Normal file
13
packages/dsc-ts/tsconfig.json
Normal 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"]
|
||||
}
|
||||
5
packages/example-config/tsconfig.json
Normal file
5
packages/example-config/tsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["winos-config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"with": ["//#lint:biome:check", "typecheck"]
|
||||
},
|
||||
"typecheck": {},
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
|
||||
"//#lint:biome": {},
|
||||
"//#lint:biome:check": {}
|
||||
|
||||
Reference in New Issue
Block a user