dynamic config
This commit is contained in:
5
bun.lock
5
bun.lock
@@ -36,10 +36,7 @@
|
|||||||
"name": "example-config",
|
"name": "example-config",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/cli": "catalog:",
|
"dsc-ts": "workspace:*",
|
||||||
"@effect/platform": "catalog:",
|
|
||||||
"@effect/platform-bun": "catalog:",
|
|
||||||
"effect": "catalog:",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "catalog:",
|
"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",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "bun@1.3.3",
|
"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": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit",
|
"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-types": "bun run scripts/gen-dsc-types.ts",
|
||||||
"gen:dsc-resources-types": "bun run scripts/gen-dsc-resources-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, 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 { 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 pkg from '../package.json' with { type: 'json' };
|
||||||
|
import { type Configuration, ConfigurationSchema } from './dsl/dsl-core';
|
||||||
import {
|
import {
|
||||||
decodeAndPrettyLogSetResult,
|
decodeAndPrettyLogSetResult,
|
||||||
decodeAndPrettyLogTestResult,
|
decodeAndPrettyLogTestResult,
|
||||||
} from './dsc-utils';
|
} from './utils/dsc-utils';
|
||||||
import { CommandUtils } from './utils';
|
import { CommandUtils } from './utils/utils';
|
||||||
|
|
||||||
const runDscConfig = (
|
const runDscConfig = (
|
||||||
subcommand: 'set' | 'test',
|
subcommand: 'set' | 'test',
|
||||||
|
machineConfig: Configuration,
|
||||||
options: { whatIf?: boolean } = {},
|
options: { whatIf?: boolean } = {},
|
||||||
) =>
|
) =>
|
||||||
Effect.gen(function* () {
|
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'),
|
Options.withDefault('winos-config.ts'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -65,8 +125,9 @@ const setCommand = Command.make(
|
|||||||
},
|
},
|
||||||
({ configPathOption }) =>
|
({ configPathOption }) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const config = yield* loadConfig(configPathOption);
|
||||||
yield* Effect.log('Applying configuration changes...');
|
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 }) =>
|
({ configPathOption }) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
const config = yield* loadConfig(configPathOption);
|
||||||
yield* Effect.log('Running configuration test...');
|
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 { Console, Effect, Schema as S } from 'effect';
|
||||||
import type { ResourceSetResponse } from './dsc-schema-types/configuration-set-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 { ConfigurationSetResult as ConfigurationSetResultSchema } from '../dsc-schema-types/configuration-set-result.gen';
|
||||||
import type { ResourceTestResponse } from './dsc-schema-types/configuration-test-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 { ConfigurationTestResult as ConfigurationTestResultSchema } from '../dsc-schema-types/configuration-test-result.gen';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes and pretty-logs the result of a DSC configuration test operation.
|
* 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({
|
export default defineConfig(async () => {
|
||||||
$schema: 'https://aka.ms/dsc/schemas/v3/config/document.json',
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
resources: [
|
return {
|
||||||
// {
|
resources: [
|
||||||
// name: 'example-registry-key',
|
// {
|
||||||
// type: 'Microsoft.Windows/Registry',
|
// name: 'example-registry-key',
|
||||||
// properties: {
|
// type: 'Microsoft.Windows/Registry',
|
||||||
// keyPath: 'HKCU\\Software\\WinosConfig',
|
// properties: {
|
||||||
// valueName: 'Version',
|
// keyPath: 'HKCU\\Software\\WinosConfig',
|
||||||
// valueData: {
|
// valueName: 'Version',
|
||||||
// String: pkg.version,
|
// valueData: {
|
||||||
// },
|
// String: pkg.version,
|
||||||
// _exist: true,
|
// },
|
||||||
// },
|
// _exist: true,
|
||||||
// },
|
// },
|
||||||
defineWingetPackage({ id: 'BurntSushi.ripgrep.MSVC' }),
|
// },
|
||||||
],
|
wingetPackage({ id: 'BurntSushi.ripgrep.MSVC' }),
|
||||||
|
],
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export default machineConfig;
|
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
"with": ["//#lint:biome:check", "typecheck"]
|
"with": ["//#lint:biome:check", "typecheck"]
|
||||||
},
|
},
|
||||||
"typecheck": {},
|
"typecheck": {},
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": ["dist/**"]
|
||||||
|
},
|
||||||
|
|
||||||
"//#lint:biome": {},
|
"//#lint:biome": {},
|
||||||
"//#lint:biome:check": {}
|
"//#lint:biome:check": {}
|
||||||
|
|||||||
Reference in New Issue
Block a user