@maestro-js/agent-skills 1.0.0-alpha.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,397 @@
1
+ ---
2
+ name: cache
3
+ description: "Key-value caching with TTL, stale-while-revalidate, distributed locking, cache-aside pattern, atomic increment/decrement, and set-if-not-exists (add) for @maestro-js/cache. Use when working with @maestro-js/cache, adding caching to services, implementing distributed locks, using the remember/SWR pattern, incrementing or decrementing counters, adding values only if absent, configuring Redis or MockRedis drivers, or following the canonical Provider pattern from service-registry."
4
+ ---
5
+
6
+ # @maestro-js/cache
7
+
8
+ Key-value caching with optional TTL, stale-while-revalidate (SWR), distributed locking, and
9
+ cache-aside (`remember`) pattern. This is the canonical example of the Maestro Provider pattern.
10
+
11
+ ## Quick Setup
12
+
13
+ ```ts
14
+ import { Cache } from '@maestro-js/cache'
15
+
16
+ // Create and register a cache instance
17
+ const cacheService = Cache.Provider.create({
18
+ driver: Cache.drivers.redis({ url: 'redis-host', port: 6379 })
19
+ })
20
+ Cache.Provider.register('default', cacheService)
21
+
22
+ // Use directly via the facade
23
+ await Cache.set('user:123', { name: 'Alice' }, 3600)
24
+ const user = await Cache.get<User>('user:123')
25
+ ```
26
+
27
+ ## Provider Pattern
28
+
29
+ Cache follows the standard Maestro Provider pattern built on `@maestro-js/service-registry`:
30
+
31
+ 1. **`Cache.Provider.create(config)`** -- factory that instantiates a cache service with a driver
32
+ 2. **`Cache.Provider.register(name, service)`** -- register the instance in a named registry
33
+ 3. **`Cache.provider(key)`** -- resolve a named instance (deferred via Proxy)
34
+ 4. **Facade** -- `Cache.get()`, `Cache.set()`, etc. spread from `provider('default')`
35
+ 5. **`Cache.drivers`** -- factory functions for pluggable backends (`redis`, `mockRedis`)
36
+
37
+ Extend provider keys via declaration merging:
38
+
39
+ ```ts
40
+ declare module '@maestro-js/cache' {
41
+ namespace Cache.Provider {
42
+ interface Keys {
43
+ default: unknown
44
+ sessions: unknown
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ Source: `packages/cache/src/index.ts`
51
+
52
+ ## Drivers
53
+
54
+ ### Redis
55
+
56
+ Production driver backed by ioredis. Pass connection details and optional tuning:
57
+
58
+ ```ts
59
+ Cache.drivers.redis({
60
+ url: 'redis-host',
61
+ port: 6379,
62
+ operationTimeout: 5000, // ms, default 5000
63
+ connectTimeout: 10000, // ms, default 10000
64
+ keyPrefix: 'myapp:', // optional prefix for all keys
65
+ logger: customLogger // optional Log.LogFunctions
66
+ })
67
+ ```
68
+
69
+ The returned driver exposes a `connection` property (the raw ioredis `Redis` instance) for
70
+ advanced use cases.
71
+
72
+ Config interface:
73
+
74
+ | Field | Type | Default | Description |
75
+ |--------------------|-------------------|---------|-------------------------------|
76
+ | `url` | `string` | -- | Redis host |
77
+ | `port` | `number` | -- | Redis port |
78
+ | `operationTimeout` | `number` | `5000` | Per-operation timeout (ms) |
79
+ | `connectTimeout` | `number` | `10000` | Connection timeout (ms) |
80
+ | `keyPrefix` | `string` | `''` | Prefix applied to all keys |
81
+ | `logger` | `Log.LogFunctions`| -- | Optional structured logger |
82
+
83
+ Source: `packages/cache/src/redis-cache-driver.ts`
84
+
85
+ ### MockRedis
86
+
87
+ In-memory driver using `ioredis-mock` for testing. Same interface as Redis:
88
+
89
+ ```ts
90
+ Cache.drivers.mockRedis({
91
+ keyPrefix: 'test:', // optional
92
+ logger: customLogger // optional
93
+ })
94
+ ```
95
+
96
+ Source: `packages/cache/src/mock-redis-cache-driver.ts`
97
+
98
+ ## API Reference
99
+
100
+ All values must be JSON-serializable. TTL is always in seconds; pass `null` to persist forever.
101
+
102
+ ### Core Methods
103
+
104
+ ```ts
105
+ Cache.has(key: string): Promise<boolean>
106
+ ```
107
+ Return `true` if the key exists in the cache.
108
+
109
+ ```ts
110
+ Cache.get<T>(key: string): Promise<T | null>
111
+ ```
112
+ Retrieve a value. Return `null` on miss.
113
+
114
+ ```ts
115
+ Cache.set(key: string, value: any, ttlSeconds: number | null): Promise<void>
116
+ ```
117
+ Store a value with TTL in seconds. Pass `null` for no expiration.
118
+
119
+ ```ts
120
+ Cache.del(key: string): Promise<void>
121
+ ```
122
+ Remove a key from the cache.
123
+
124
+ ```ts
125
+ Cache.flush(): Promise<void>
126
+ ```
127
+ Remove all entries. Ignores key prefix -- affects the entire store.
128
+
129
+ ### Atomic Operations
130
+
131
+ ```ts
132
+ Cache.add(key: string, value: any, ttlSeconds: number): Promise<boolean>
133
+ ```
134
+ Store a value only if the key does not already exist. Returns `true` if added, `false` if the key
135
+ was already present. The value is JSON-serialized (compatible with `get`).
136
+
137
+ ```ts
138
+ Cache.increment(key: string, value?: number): Promise<number>
139
+ ```
140
+ Atomically increment a numeric value by `value` (default `1`). Returns the new value.
141
+
142
+ ```ts
143
+ Cache.decrement(key: string, value?: number): Promise<number>
144
+ ```
145
+ Atomically decrement a numeric value by `value` (default `1`). Returns the new value.
146
+
147
+ > **Warning:** `increment` and `decrement` use Redis `INCRBY`/`DECRBY` on raw integers, not
148
+ > JSON-serialized values. Values stored with `set` (which JSON-serializes) cannot be incremented.
149
+ > Conversely, values created by `increment`/`decrement` are raw numbers and will come back as plain
150
+ > numbers from `get`, not wrapped in JSON.
151
+
152
+ ```ts
153
+ // Rate-limit counter
154
+ await Cache.increment('rate:user:42')
155
+ const count = await Cache.increment('rate:user:42')
156
+ if (count > 100) throw new Error('Rate limit exceeded')
157
+
158
+ // Add-if-absent: claim a one-time token
159
+ const claimed = await Cache.add('token:abc123', { userId: 42 }, 300)
160
+ if (!claimed) throw new Error('Token already used')
161
+ ```
162
+
163
+ ### remember (Cache-Aside)
164
+
165
+ ```ts
166
+ Cache.remember<T>(key: string, options: RememberOptions, fn: () => Promise<T>): Promise<T>
167
+ ```
168
+
169
+ Retrieve `key` from cache. On miss, call `fn`, cache the result, and return it.
170
+
171
+ **RememberOptions** accepts:
172
+ - `number` -- TTL in seconds
173
+ - `null` -- persist forever
174
+ - `DurationString` -- e.g. `'5 minutes'`, `'1 hour'`, `'30 seconds'`, `'2 days'`
175
+ - `{ ttl: number | DurationString | null, swr: number | DurationString }` -- TTL with
176
+ stale-while-revalidate window
177
+
178
+ ```ts
179
+ // Simple TTL
180
+ await Cache.remember('user:1', 60, async () => fetchUser(1))
181
+
182
+ // Duration string
183
+ await Cache.remember('config', '5 minutes', async () => loadConfig())
184
+
185
+ // Forever
186
+ await Cache.remember('constants', null, async () => getConstants())
187
+
188
+ // Stale-while-revalidate
189
+ await Cache.remember('feed', { ttl: 60, swr: 30 }, async () => buildFeed())
190
+ await Cache.remember('feed', { ttl: '1 minute', swr: '30 seconds' }, async () => buildFeed())
191
+ ```
192
+
193
+ SWR behavior: when the TTL expires but the entry is within the SWR window, the stale value is
194
+ returned immediately and `fn` runs in the background to refresh the cache. Background refresh
195
+ errors are silently swallowed (the stale value remains). Only one background refresh per key runs
196
+ at a time.
197
+
198
+ Do not mix `Cache.set()` and `Cache.remember()` on the same key -- `remember` wraps values in an
199
+ internal envelope (`__remember` marker) and throws if it encounters a raw value.
200
+
201
+ ### item (Typed Key Handle)
202
+
203
+ ```ts
204
+ Cache.item<T>(key: string)
205
+ ```
206
+
207
+ Return a typed handle scoped to a single key:
208
+
209
+ ```ts
210
+ const user = Cache.item<User>('user:456')
211
+ await user.set({ name: 'Bob' }, 7200)
212
+ await user.exists() // true
213
+ await user.get() // { name: 'Bob' }
214
+ await user.del()
215
+ await user.remember(60, async () => fetchUser(456))
216
+ await user.remember('5 minutes', async () => fetchUser(456))
217
+ await user.remember({ ttl: 1, swr: 10 }, async () => fetchUser(456))
218
+ ```
219
+
220
+ ### lock (Distributed Locking)
221
+
222
+ ```ts
223
+ Cache.lock(name: string, ttlSeconds: number | null): Cache.Lock
224
+ ```
225
+
226
+ Create a distributed lock backed by Redis `SET NX`. Lock methods:
227
+
228
+ ```ts
229
+ const lock = Cache.lock('process-orders', 30)
230
+
231
+ await lock.get() // true if acquired, false if held by another
232
+ await lock.isAvailable() // true if no one holds the lock
233
+ await lock.release() // release if owned by this caller (atomic Lua script)
234
+ await lock.forceRelease() // release regardless of owner
235
+ await lock.block(5) // retry acquiring for up to 5 seconds, throw on timeout
236
+ ```
237
+
238
+ Pass `null` for TTL to create a lock with no automatic expiration.
239
+
240
+ `block()` polls every 250ms and also listens for Redis pub/sub release events for faster
241
+ acquisition. Throws `Error('Could not acquire lock: <name>')` on timeout.
242
+
243
+ ## Common Patterns
244
+
245
+ ### Cache-aside with TTL
246
+
247
+ ```ts
248
+ const user = await Cache.remember('user:123', 3600, async () => {
249
+ return await db.query('SELECT * FROM users WHERE id = 123')
250
+ })
251
+ ```
252
+
253
+ ### Stale-while-revalidate
254
+
255
+ Serve stale data instantly while refreshing in the background:
256
+
257
+ ```ts
258
+ const dashboard = await Cache.remember(
259
+ 'dashboard:stats',
260
+ { ttl: '5 minutes', swr: '1 minute' },
261
+ async () => computeExpensiveStats()
262
+ )
263
+ ```
264
+
265
+ ### Distributed mutex
266
+
267
+ Ensure only one process runs a task at a time:
268
+
269
+ ```ts
270
+ const lock = Cache.lock('import-job', 60)
271
+ const acquired = await lock.get()
272
+ if (acquired) {
273
+ try {
274
+ await runImport()
275
+ } finally {
276
+ await lock.release()
277
+ }
278
+ }
279
+ ```
280
+
281
+ ### Blocking lock with timeout
282
+
283
+ Wait for availability:
284
+
285
+ ```ts
286
+ const lock = Cache.lock('critical-section', 30)
287
+ await lock.block(10) // wait up to 10 seconds
288
+ try {
289
+ await doCriticalWork()
290
+ } finally {
291
+ await lock.release()
292
+ }
293
+ ```
294
+
295
+ ### Named provider instances
296
+
297
+ ```ts
298
+ const sessions = Cache.Provider.create({
299
+ driver: Cache.drivers.redis({ url: 'redis-host', port: 6379, keyPrefix: 'sess:' })
300
+ })
301
+ Cache.Provider.register('sessions', sessions)
302
+
303
+ // Resolve by name
304
+ const sessCache = Cache.provider('sessions')
305
+ await sessCache.set('abc', { userId: 1 }, 1800)
306
+ ```
307
+
308
+ ## Testing
309
+
310
+ Use MockRedis driver in tests. Flush before each test to ensure isolation:
311
+
312
+ ```ts
313
+ import { Cache } from '@maestro-js/cache'
314
+ import { test } from 'beartest-js'
315
+ import { expect } from 'expect'
316
+
317
+ const cache = Cache.Provider.create({ driver: Cache.drivers.mockRedis({}) })
318
+
319
+ test.beforeEach(async () => {
320
+ await cache.flush()
321
+ })
322
+
323
+ test('cache stores and retrieves values', async () => {
324
+ await cache.set('key', { a: 1 }, null)
325
+ expect(await cache.has('key')).toBeTruthy()
326
+ expect(await cache.get('key')).toEqual({ a: 1 })
327
+ })
328
+
329
+ test('remember caches on miss', async () => {
330
+ const result = await cache.remember('user:1', 60, async () => ({ name: 'Alice' }))
331
+ expect(result).toEqual({ name: 'Alice' })
332
+
333
+ let called = false
334
+ const cached = await cache.remember('user:1', 60, async () => {
335
+ called = true
336
+ return { name: 'Bob' }
337
+ })
338
+ expect(cached).toEqual({ name: 'Alice' })
339
+ expect(called).toBe(false)
340
+ })
341
+
342
+ test('locks acquire and release', async () => {
343
+ const lock = cache.lock('test-lock', null)
344
+ expect(await lock.isAvailable()).toBe(true)
345
+ await lock.get()
346
+ expect(await lock.isAvailable()).toBe(false)
347
+ await lock.release()
348
+ expect(await lock.isAvailable()).toBe(true)
349
+ })
350
+ ```
351
+
352
+ Run tests:
353
+
354
+ ```bash
355
+ pnpm --filter @maestro-js/cache test # all cache tests
356
+ cd packages/cache && npx beartest ./tests/cache.test.ts # single file
357
+ ```
358
+
359
+ ## Cross-Package Integration
360
+
361
+ - **Depends on**: `@maestro-js/service-registry` (Provider pattern foundation),
362
+ `@maestro-js/log` (structured logging in drivers)
363
+ - **Used by**: `@maestro-js/cache-control` (HTTP cache-control headers backed by cache)
364
+ - **Complements**: `@maestro-js/context` (per-request via AsyncLocalStorage; cache is
365
+ cross-request shared state)
366
+
367
+ ## Driver Interface
368
+
369
+ Implement `Cache.Driver` to create a custom backend:
370
+
371
+ ```ts
372
+ interface CacheDriver {
373
+ has(key: string): Promise<boolean>
374
+ get(key: string): Promise<any | null>
375
+ set(key: string, value: any, ttlSeconds: number): Promise<unknown>
376
+ add(key: string, value: any, ttlSeconds: number): Promise<boolean>
377
+ setForever(key: string, value: any): Promise<unknown>
378
+ del(key: string): Promise<unknown>
379
+ increment(key: string, value: number): Promise<number>
380
+ decrement(key: string, value: number): Promise<number>
381
+ flush(): Promise<unknown>
382
+ getPrefix(): string
383
+ lock(name: string, ttlSeconds: number | null): Cache.Lock
384
+ }
385
+ ```
386
+
387
+ Source: `packages/cache/src/cache-types.ts`
388
+
389
+ ## Key Source Files
390
+
391
+ - `packages/cache/src/index.ts` -- main export, Provider pattern, `create()`, `remember()` logic
392
+ - `packages/cache/src/cache-types.ts` -- `CacheDriver` and `CacheLock` interfaces
393
+ - `packages/cache/src/redis-cache-driver.ts` -- production Redis driver
394
+ - `packages/cache/src/mock-redis-cache-driver.ts` -- in-memory mock driver for tests
395
+ - `packages/cache/src/duration.ts` -- `DurationString` type and parser
396
+ - `packages/cache/tests/cache.test.ts` -- core cache and remember/SWR tests
397
+ - `packages/cache/tests/cache-locks.test.ts` -- distributed lock tests
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: config
3
+ description: "Configuration loader for .env files and TypeScript/JavaScript config modules with AES-256-GCM encryption support. Use when working with @maestro-js/config, including loading config into process.env, reading/writing encrypted .env files, creating config modules, deriving computed env values, and generating encryption keys. Key capabilities: load config files, parse .env files (plaintext or encrypted), encrypt/decrypt env files, generate encryption keys, resolve config modules (named exports, default exports, sync/async functions), type-safe process.env via Config.Resolve utility type, and auto-load via node --import @maestro-js/config/load."
4
+ ---
5
+
6
+ # @maestro-js/config
7
+
8
+ Standalone configuration loader that reads `.env` files (plaintext or AES-256-GCM encrypted), imports a TypeScript or JavaScript config module, and writes the resolved key-value pairs into `process.env`. Zero `@maestro-js` dependencies.
9
+
10
+ ## Quick Setup
11
+
12
+ ```ts
13
+ import { Config } from '@maestro-js/config'
14
+
15
+ // Load config into process.env (existing keys are NOT overwritten)
16
+ await Config.loadConfigFile({
17
+ rootDir: process.cwd(),
18
+ configName: 'app',
19
+ envName: 'production',
20
+ encryptionKey: process.env.MAESTRO_ENV_ENCRYPTION_KEY ?? null
21
+ })
22
+ ```
23
+
24
+ Or auto-load at startup without application code:
25
+
26
+ ```bash
27
+ node --import @maestro-js/config/load --config app --env production --root /path/to/project
28
+ ```
29
+
30
+ ## API Reference
31
+
32
+ ### ConfigOptions
33
+
34
+ Every primary function accepts this options object:
35
+
36
+ ```ts
37
+ type ConfigOptions = {
38
+ rootDir: string // Project root directory
39
+ configName: string // Config file name without extension (looked up in <rootDir>/config/)
40
+ envName: string | null // Environment name (.env.<envName>) or null for .env
41
+ encryptionKey: string | null // 64-char hex key for encrypted .env files, or null
42
+ }
43
+ ```
44
+
45
+ ### Config.loadConfigFile(options: ConfigOptions): Promise<void>
46
+
47
+ Read the `.env` file, import the config file, and write the resolved values into `process.env`. Keys already present in `process.env` are not overwritten.
48
+
49
+ ### Config.getConfig(options: ConfigOptions): Promise<NodeJS.Dict<string>>
50
+
51
+ Same as `loadConfigFile` but returns the resolved config object without modifying `process.env`.
52
+
53
+ ### Config.getEnvVars(options): NodeJS.Dict<string>
54
+
55
+ Read and parse an env file (plaintext or encrypted). Returns `{}` if no env file is found. Throws if an encrypted file is found but no `encryptionKey` is provided.
56
+
57
+ ```ts
58
+ Config.getEnvVars({ rootDir: '.', envName: null, encryptionKey: null })
59
+ // => { FOO: 'bar', BAZ: 'qux' }
60
+ ```
61
+
62
+ ### Config.resolveConfig(module: any): Promise<NodeJS.Dict<string>>
63
+
64
+ Normalize an imported config module into a flat string dictionary. Handles four export shapes: named exports, default object, default sync function, and default async function. Numbers and booleans are stringified. Objects and arrays throw.
65
+
66
+ ### Config.generateEncryptedEnvFile(options): void
67
+
68
+ Read a plaintext `.env` file and write its AES-256-GCM encrypted counterpart (`.env.encrypted` or `.env.<envName>.encrypted`).
69
+
70
+ ```ts
71
+ Config.generateEncryptedEnvFile({
72
+ rootDir: process.cwd(),
73
+ envName: null,
74
+ encryptionKey: key
75
+ })
76
+ ```
77
+
78
+ ### Config.generateDecryptedEnvFile(options): void
79
+
80
+ Read an encrypted `.env.encrypted` file and write the decrypted plaintext. Throws if the target file already exists unless `force: true`.
81
+
82
+ ```ts
83
+ Config.generateDecryptedEnvFile({
84
+ rootDir: process.cwd(),
85
+ envName: 'production',
86
+ encryptionKey: key,
87
+ force: true
88
+ })
89
+ ```
90
+
91
+ ### Config.generateEncryptionKey(): string
92
+
93
+ Generate a cryptographically random 32-byte key as a 64-character hex string.
94
+
95
+ ```ts
96
+ const key = Config.generateEncryptionKey()
97
+ // => '7a3f8b2c...' (64 hex chars)
98
+ ```
99
+
100
+ ### Config.Resolve<T> (type utility)
101
+
102
+ Extract the resolved key-value shape from a config module type. Use in an `environment.d.ts` file to make `process.env` type-safe.
103
+
104
+ ```ts
105
+ import type { Config } from '@maestro-js/config'
106
+ import type appConfig from './config/app.ts'
107
+
108
+ declare global {
109
+ namespace NodeJS {
110
+ interface ProcessEnv extends Config.Resolve<typeof appConfig> {}
111
+ }
112
+ }
113
+ ```
114
+
115
+ ## Common Patterns
116
+
117
+ ### Config file with named exports
118
+
119
+ Place config files in `<rootDir>/config/`. Supported extensions: `.mts`, `.cts`, `.ts`, `.mjs`, `.cjs`, `.js`.
120
+
121
+ ```ts
122
+ // config/app.ts
123
+ export const DB_HOST = 'localhost'
124
+ export const DB_PORT = '3306'
125
+ export const DEBUG = true // stringified to 'true'
126
+ ```
127
+
128
+ ### Config file as a function (derive values from .env)
129
+
130
+ When the config exports a function, it receives the merged env vars (process.env values take precedence over .env file values).
131
+
132
+ ```ts
133
+ // config/app.ts
134
+ export default (env: Record<string, string | undefined>) => ({
135
+ DATABASE_URL: `mysql://${env.DB_USER}:${env.DB_PASS}@${env.DB_HOST}:${env.DB_PORT}/${env.DB_NAME}`,
136
+ API_KEY: env.API_KEY
137
+ })
138
+ ```
139
+
140
+ ### Async config function
141
+
142
+ ```ts
143
+ // config/app.ts
144
+ export default async (env: Record<string, string | undefined>) => {
145
+ return {
146
+ SECRET: env.SECRET,
147
+ COMPUTED: await someAsyncOperation()
148
+ }
149
+ }
150
+ ```
151
+
152
+ ### Env file resolution order
153
+
154
+ When `envName` is provided (e.g., `'production'`), files are checked in this order:
155
+ 1. `.env.production`
156
+ 2. `.env.production.encrypted`
157
+ 3. `.env`
158
+ 4. `.env.encrypted`
159
+
160
+ When `envName` is null:
161
+ 1. `.env`
162
+ 2. `.env.encrypted`
163
+
164
+ ### Encryption round-trip
165
+
166
+ ```ts
167
+ const key = Config.generateEncryptionKey()
168
+
169
+ // Encrypt: reads .env, writes .env.encrypted
170
+ Config.generateEncryptedEnvFile({ rootDir: '.', envName: null, encryptionKey: key })
171
+
172
+ // Decrypt: reads .env.encrypted, writes .env
173
+ Config.generateDecryptedEnvFile({ rootDir: '.', envName: null, encryptionKey: key, force: true })
174
+ ```
175
+
176
+ Encrypted values use the format `<iv_base64>.<authTag_base64>.<ciphertext_base64>` per key.
177
+
178
+ ### Auto-load entry point
179
+
180
+ The `@maestro-js/config/load` export is a standalone script intended for `node --import`. It parses CLI arguments and calls `Config.loadConfigFile`.
181
+
182
+ CLI flags:
183
+ - `--config <name>` (required) -- config file name without extension
184
+ - `--env <name>` -- environment name
185
+ - `--key <hex>` -- encryption key (falls back to `MAESTRO_ENV_ENCRYPTION_KEY` env var)
186
+ - `--root <path>` -- project root (defaults to `process.cwd()`)
187
+
188
+ ## Cross-Package Integration
189
+
190
+ Config is a standalone package with zero `@maestro-js` dependencies. It is used by:
191
+
192
+ - **CLI** -- The Maestro CLI uses `Config.generateEncryptedEnvFile`, `Config.generateDecryptedEnvFile`, and `Config.generateEncryptionKey` for the `env:encrypt`, `env:decrypt`, and `env:gen-key` commands.
193
+ - **Application boot** -- Applications use `Config.loadConfigFile` (or `node --import @maestro-js/config/load`) to populate `process.env` before initializing other Maestro packages that read from `process.env`.
194
+ - **Type safety** -- The `Config.Resolve` utility type lets applications create a typed `process.env` derived from the config module shape.
195
+
196
+ ## Testing
197
+
198
+ ```bash
199
+ pnpm --filter @maestro-js/config test
200
+ cd packages/config && npx beartest ./tests/config.test.ts
201
+ ```
202
+
203
+ Tests use `beartest-js` with `expect`. They create temporary directories, write config/env files, and verify loading, encryption round-trips, and error cases.