@crustjs/store 0.0.1 → 0.0.3

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.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,251 @@
1
1
  /**
2
+ * Recursively makes all properties of `T` optional.
3
+ *
4
+ * - Plain objects are recursed into — each nested key becomes optional.
5
+ * - Arrays are left as-is (replaced wholesale, not partially updated).
6
+ * - Primitives pass through unchanged.
7
+ *
8
+ * Used by {@link Store.patch} for deep partial updates.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * type Config = { ui: { theme: string; fontSize: number }; verbose: boolean };
13
+ * type Partial = DeepPartial<Config>;
14
+ * // → { ui?: { theme?: string; fontSize?: number }; verbose?: boolean }
15
+ * ```
16
+ */
17
+ type DeepPartial<T> = T extends readonly unknown[] ? T : T extends Record<string, unknown> ? { [K in keyof T]? : DeepPartial<T[K]> } : T;
18
+ /**
19
+ * Result returned by a {@link StoreValidator} when validation succeeds.
20
+ *
21
+ * Carries the (possibly transformed) config value from the schema.
22
+ */
23
+ interface StoreValidatorSuccess<T> {
24
+ readonly ok: true;
25
+ readonly value: T;
26
+ }
27
+ /**
28
+ * A single validation issue reported by a {@link StoreValidator}.
29
+ *
30
+ * Compatible with the canonical `ValidationIssue` shape used across the Crust
31
+ * validation platform.
32
+ */
33
+ interface StoreValidatorIssue {
34
+ /** Human-readable description of the validation failure. */
35
+ message: string;
36
+ /** Dot-path to the invalid field, or empty string for root-level issues. */
37
+ path: string;
38
+ }
39
+ /**
40
+ * Result returned by a {@link StoreValidator} when validation fails.
41
+ *
42
+ * Carries structured issues for programmatic handling and error rendering.
43
+ */
44
+ interface StoreValidatorFailure {
45
+ readonly ok: false;
46
+ readonly issues: readonly StoreValidatorIssue[];
47
+ }
48
+ /**
49
+ * Discriminated union result from a {@link StoreValidator}.
50
+ */
51
+ type StoreValidatorResult<T> = StoreValidatorSuccess<T> | StoreValidatorFailure;
52
+ /**
53
+ * A provider-agnostic validator function for full config objects.
54
+ *
55
+ * Accepts a config value and returns a discriminated result indicating
56
+ * success (with a possibly-transformed value) or failure (with structured issues).
57
+ *
58
+ * Store adapters in `@crustjs/validate` produce functions matching this contract
59
+ * from Standard Schema, Zod, or Effect schemas.
60
+ *
61
+ * @typeParam T - The expected config type.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const validator: StoreValidator<Config> = (config) => {
66
+ * if (typeof config.theme !== "string") {
67
+ * return { ok: false, issues: [{ message: "Expected string", path: "theme" }] };
68
+ * }
69
+ * return { ok: true, value: config };
70
+ * };
71
+ * ```
72
+ */
73
+ type StoreValidator<T> = (value: unknown) => StoreValidatorResult<T> | Promise<StoreValidatorResult<T>>;
74
+ /**
75
+ * Options for {@link createStore}.
76
+ *
77
+ * @typeParam T - The store data shape, inferred from `defaults`.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const store = createStore({
82
+ * dirPath: configDir("my-cli"),
83
+ * defaults: {
84
+ * ui: { theme: "light", fontSize: 14 },
85
+ * verbose: false,
86
+ * tags: ["default"],
87
+ * },
88
+ * });
89
+ * ```
90
+ */
91
+ interface CreateStoreOptions<T extends Record<string, unknown>> {
92
+ /**
93
+ * Absolute directory path where the store JSON file is persisted.
94
+ *
95
+ * Use a path helper ({@link configDir}, {@link dataDir}, etc.) to resolve
96
+ * the platform-standard directory from an app name.
97
+ */
98
+ dirPath: string;
99
+ /**
100
+ * Store name used as the JSON filename in the directory.
101
+ *
102
+ * Defaults to `"config"`, producing `config.json`. Set to a different value
103
+ * to create multiple stores under the same directory
104
+ * (e.g. `name: "auth"` → `auth.json`).
105
+ *
106
+ * Must not contain path separators or the `.json` extension.
107
+ */
108
+ name?: string;
109
+ /**
110
+ * Default values for the store. Defines the data shape and provides
111
+ * fallback values when no persisted file exists or keys are missing.
112
+ *
113
+ * The TypeScript type of `defaults` determines the store's `T` parameter.
114
+ */
115
+ defaults: T;
116
+ /**
117
+ * Optional validator function applied to config objects during read, write,
118
+ * update, and patch operations.
119
+ *
120
+ * When provided, validation is **strict by default**: invalid config will
121
+ * cause a `CrustStoreError` with `VALIDATION` code to be thrown on both
122
+ * persistence (write/update/patch) and load (read) paths.
123
+ *
124
+ * Use store adapters from `@crustjs/validate/standard`, `@crustjs/validate/zod`,
125
+ * or `@crustjs/validate/effect` to create validators from schema definitions.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * import { storeValidator } from "@crustjs/validate/zod";
130
+ * import { z } from "zod";
131
+ *
132
+ * const store = createStore({
133
+ * dirPath: configDir("my-cli"),
134
+ * defaults: { theme: "light", verbose: false },
135
+ * validator: storeValidator(z.object({ theme: z.enum(["light", "dark"]) })),
136
+ * });
137
+ * ```
138
+ */
139
+ validator?: StoreValidator<T>;
140
+ /**
141
+ * When `true` (the default), persisted keys not present in `defaults`
142
+ * are dropped on read. Set to `false` to preserve unknown keys.
143
+ *
144
+ * @default true
145
+ */
146
+ pruneUnknown?: boolean;
147
+ }
148
+ /**
149
+ * Receives the current effective state and returns an updated state.
150
+ *
151
+ * Used by {@link Store.update} to apply mutations atomically.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * await store.update((current) => ({
156
+ * ...current,
157
+ * ui: { ...current.ui, theme: "dark" },
158
+ * }));
159
+ * ```
160
+ */
161
+ type StoreUpdater<T> = (current: T) => NoInfer<T>;
162
+ /**
163
+ * A typed async store returned by {@link createStore}.
164
+ *
165
+ * Provides `read`, `write`, `update`, `patch`, and `reset` operations for a
166
+ * typed state object persisted as JSON on the local filesystem.
167
+ *
168
+ * @typeParam T - The store data shape, inferred from `defaults`.
169
+ *
170
+ * @example
171
+ * ```ts
172
+ * const store = createStore({
173
+ * dirPath: configDir("my-cli"),
174
+ * defaults: {
175
+ * ui: { theme: "light", fontSize: 14 },
176
+ * verbose: false,
177
+ * },
178
+ * });
179
+ *
180
+ * const state = await store.read();
181
+ * await store.write({ ui: { theme: "dark", fontSize: 14 }, verbose: true });
182
+ * await store.update((s) => ({ ...s, verbose: false }));
183
+ * await store.patch({ ui: { theme: "dark" } });
184
+ * await store.reset();
185
+ * ```
186
+ */
187
+ interface Store<T> {
188
+ /**
189
+ * Reads the persisted state, merging defaults for missing keys.
190
+ *
191
+ * Always returns a complete `T` — missing persisted keys are filled
192
+ * from `defaults`.
193
+ *
194
+ * When a validator is configured, the merged config is validated after
195
+ * defaults are applied. Invalid persisted config fails loudly.
196
+ *
197
+ * @returns The effective config value (possibly transformed by the validator).
198
+ * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
199
+ * @throws {CrustStoreError} `VALIDATION` if the config fails validation.
200
+ * @throws {CrustStoreError} `IO` on filesystem read failures.
201
+ */
202
+ read(): Promise<T>;
203
+ /**
204
+ * Atomically persists the full state object.
205
+ *
206
+ * When a validator is configured, the config is validated before persistence.
207
+ *
208
+ * @param state - The complete state to persist.
209
+ * @throws {CrustStoreError} `VALIDATION` if the config fails validation.
210
+ * @throws {CrustStoreError} `IO` on filesystem write failures.
211
+ */
212
+ write(state: NoInfer<T>): Promise<void>;
213
+ /**
214
+ * Reads current effective state, applies the updater, and persists.
215
+ *
216
+ * When a validator is configured, the updated config is validated before
217
+ * persistence. The read step during update does **not** validate separately
218
+ * to avoid double-validation overhead — the `current` value passed to the
219
+ * updater may not have been validated (e.g. if the persisted file was
220
+ * manually edited).
221
+ *
222
+ * @param updater - Function receiving current state and returning updated state.
223
+ * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
224
+ * @throws {CrustStoreError} `VALIDATION` if the updated config fails validation.
225
+ * @throws {CrustStoreError} `IO` on filesystem failures.
226
+ */
227
+ update(updater: StoreUpdater<T>): Promise<void>;
228
+ /**
229
+ * Applies a deep partial update to the current state and persists.
230
+ *
231
+ * Only the provided keys are updated; everything else is preserved.
232
+ * Arrays are replaced wholesale (not merged element-by-element).
233
+ *
234
+ * @param partial - A deep-partial subset of the state to merge in.
235
+ * @throws {CrustStoreError} `VALIDATION` if the merged config fails validation.
236
+ * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
237
+ * @throws {CrustStoreError} `IO` on filesystem failures.
238
+ */
239
+ patch(partial: DeepPartial<NoInfer<T>>): Promise<void>;
240
+ /**
241
+ * Removes the persisted state file, returning the store to
242
+ * defaults-on-read behavior.
243
+ *
244
+ * @throws {CrustStoreError} `IO` on filesystem deletion failures.
245
+ */
246
+ reset(): Promise<void>;
247
+ }
248
+ /**
2
249
  * Contextual details attached to a `PATH` error.
3
250
  *
4
251
  * Returned when the resolved or provided config file path is invalid.
@@ -28,6 +275,18 @@ interface IOErrorDetails {
28
275
  operation: "read" | "write" | "delete";
29
276
  }
30
277
  /**
278
+ * Contextual details attached to a `VALIDATION` error.
279
+ *
280
+ * Returned when a store validator rejects a config object during
281
+ * read, write, update, or patch operations.
282
+ */
283
+ interface ValidationErrorDetails {
284
+ /** The store operation during which validation failed. */
285
+ operation: "read" | "write" | "update" | "patch";
286
+ /** Structured validation issues for programmatic handling. */
287
+ issues: readonly StoreValidatorIssue[];
288
+ }
289
+ /**
31
290
  * Maps each {@link StoreErrorCode} to its structured details type.
32
291
  *
33
292
  * Used internally for conditional constructor parameters and type narrowing.
@@ -36,6 +295,7 @@ interface StoreErrorDetailsMap {
36
295
  PATH: PathErrorDetails;
37
296
  PARSE: ParseErrorDetails;
38
297
  IO: IOErrorDetails;
298
+ VALIDATION: ValidationErrorDetails;
39
299
  }
40
300
  /**
41
301
  * All possible error codes emitted by `@crustjs/store`.
@@ -43,6 +303,7 @@ interface StoreErrorDetailsMap {
43
303
  * - `PATH` — Invalid or unsupported config file path
44
304
  * - `PARSE` — Malformed JSON in persisted config file
45
305
  * - `IO` — Filesystem read, write, or delete failure
306
+ * - `VALIDATION` — Config object rejected by a store validator
46
307
  *
47
308
  * @example
48
309
  * ```ts
@@ -56,6 +317,11 @@ interface StoreErrorDetailsMap {
56
317
  * case "PARSE":
57
318
  * console.error(`Corrupt config at ${err.details.path}`);
58
319
  * break;
320
+ * case "VALIDATION":
321
+ * for (const issue of err.details.issues) {
322
+ * console.error(`${issue.path}: ${issue.message}`);
323
+ * }
324
+ * break;
59
325
  * case "IO":
60
326
  * console.error(`File ${err.details.operation} failed: ${err.message}`);
61
327
  * break;
@@ -68,7 +334,7 @@ type StoreErrorCode = keyof StoreErrorDetailsMap;
68
334
  /** Resolves the details type for a given {@link StoreErrorCode}. */
69
335
  type StoreErrorDetails<C extends StoreErrorCode> = StoreErrorDetailsMap[C];
70
336
  /**
71
- * A typed error thrown by `@crustjs/store` for path, parse, and IO failures.
337
+ * A typed error thrown by `@crustjs/store` for path, parse, IO, and validation failures.
72
338
  *
73
339
  * Every `CrustStoreError` carries a {@link StoreErrorCode} that identifies the failure
74
340
  * category, along with structured {@link StoreErrorDetails} for programmatic handling
@@ -143,10 +409,11 @@ interface PlatformEnv {
143
409
  * Resolves the platform-standard config directory for the given app.
144
410
  *
145
411
  * Platform conventions:
146
- * - **Linux**: `$XDG_CONFIG_HOME/<appName>` or `~/.config/<appName>`
147
- * - **macOS**: `~/Library/Application Support/<appName>`
412
+ * - **Linux / macOS**: `$XDG_CONFIG_HOME/<appName>` or `~/.config/<appName>`
148
413
  * - **Windows**: `%APPDATA%/<appName>` or `~/AppData/Roaming/<appName>`
149
414
  *
415
+ * macOS uses XDG conventions for consistency with Linux.
416
+ *
150
417
  * @param appName - Application name used as directory name. Must be a non-empty
151
418
  * string without path separators.
152
419
  * @param env - Optional platform environment override for testing.
@@ -159,241 +426,99 @@ interface PlatformEnv {
159
426
  *
160
427
  * const dir = configDir("my-cli");
161
428
  * // → "/home/user/.config/my-cli" (Linux)
162
- * // → "/Users/user/Library/Application Support/my-cli" (macOS)
429
+ * // → "/Users/user/.config/my-cli" (macOS)
163
430
  * // → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
164
431
  * ```
165
432
  */
166
433
  declare function configDir(appName: string, env?: PlatformEnv): string;
167
- /** Supported type literals for store fields. */
168
- type ValueType = "string" | "number" | "boolean";
169
- /**
170
- * Resolves a type literal to its corresponding TypeScript primitive type.
171
- *
172
- * - `"string"` → `string`
173
- * - `"number"` → `number`
174
- * - `"boolean"` → `boolean`
175
- */
176
- type ResolvePrimitive<T extends ValueType> = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : never;
177
- /** Shared fields present on every store field definition. */
178
- interface FieldDefBase {
179
- /** Human-readable description for documentation and tooling. */
180
- description?: string;
181
- }
182
- /** Base for scalar (non-array) fields — `array` must be omitted. */
183
- interface ScalarFieldBase extends FieldDefBase {
184
- /** Must be omitted for scalar fields — set to `true` for array fields. */
185
- array?: never;
186
- }
187
- /** A scalar string field. */
188
- interface StringFieldDef extends ScalarFieldBase {
189
- type: "string";
190
- /** Default string value when the field is not persisted. */
191
- default?: string;
192
- }
193
- /** A scalar number field. */
194
- interface NumberFieldDef extends ScalarFieldBase {
195
- type: "number";
196
- /** Default number value when the field is not persisted. */
197
- default?: number;
198
- }
199
- /** A scalar boolean field. */
200
- interface BooleanFieldDef extends ScalarFieldBase {
201
- type: "boolean";
202
- /** Default boolean value when the field is not persisted. */
203
- default?: boolean;
204
- }
205
- /** Base for array fields — `array` is required as `true`. */
206
- interface ArrayFieldBase extends FieldDefBase {
207
- /** Collect values into an array. */
208
- array: true;
209
- }
210
- /** An array of strings field. */
211
- interface StringArrayFieldDef extends ArrayFieldBase {
212
- type: "string";
213
- /** Default string array value when the field is not persisted. */
214
- default?: readonly string[];
215
- }
216
- /** An array of numbers field. */
217
- interface NumberArrayFieldDef extends ArrayFieldBase {
218
- type: "number";
219
- /** Default number array value when the field is not persisted. */
220
- default?: readonly number[];
221
- }
222
- /** An array of booleans field. */
223
- interface BooleanArrayFieldDef extends ArrayFieldBase {
224
- type: "boolean";
225
- /** Default boolean array value when the field is not persisted. */
226
- default?: readonly boolean[];
227
- }
228
434
  /**
229
- * Defines a single field in a store's config schema.
435
+ * Resolves the platform-standard data directory for the given app.
230
436
  *
231
- * Discriminated by `type` and `array` for type-safe `default` values.
437
+ * Platform conventions:
438
+ * - **Linux / macOS**: `$XDG_DATA_HOME/<appName>` or `~/.local/share/<appName>`
439
+ * - **Windows**: `%LOCALAPPDATA%/<appName>/Data` or `~/AppData/Local/<appName>/Data`
232
440
  *
233
- * @example
234
- * ```ts
235
- * const fields = {
236
- * theme: { type: "string", default: "light" },
237
- * verbose: { type: "boolean", default: false },
238
- * tags: { type: "string", array: true, default: [] },
239
- * token: { type: "string" }, // optional — no default
240
- * } satisfies FieldsDef;
241
- * ```
242
- */
243
- type FieldDef = StringFieldDef | NumberFieldDef | BooleanFieldDef | StringArrayFieldDef | NumberArrayFieldDef | BooleanArrayFieldDef;
244
- /** Record mapping field names to their definitions. */
245
- type FieldsDef = Record<string, FieldDef>;
246
- /**
247
- * Infer the resolved type for a single field definition.
441
+ * macOS uses XDG conventions for consistency with Linux.
248
442
  *
249
- * - **array** → `primitive[]` (or `primitive[] | undefined` if no default)
250
- * - **has default** → `primitive` (guaranteed present)
251
- * - **no default** → `primitive | undefined` (optional)
252
- */
253
- type InferFieldValue<F extends FieldDef> = F extends {
254
- array: true;
255
- } ? F extends {
256
- default: readonly ResolvePrimitive<F["type"]>[];
257
- } ? ResolvePrimitive<F["type"]>[] : ResolvePrimitive<F["type"]>[] | undefined : F extends {
258
- default: ResolvePrimitive<F["type"]>;
259
- } ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined;
260
- /**
261
- * Maps a full {@link FieldsDef} record to the inferred config object type.
443
+ * @param appName - Application name used as directory name. Must be a non-empty
444
+ * string without path separators.
445
+ * @param env - Optional platform environment override for testing.
446
+ * @returns Absolute path to the app's data directory.
447
+ * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
262
448
  *
263
449
  * @example
264
450
  * ```ts
265
- * type Config = InferStoreConfig<{
266
- * theme: { type: "string"; default: "light" };
267
- * verbose: { type: "boolean" };
268
- * }>;
269
- * // → { theme: string; verbose: boolean | undefined }
451
+ * import { dataDir } from "@crustjs/store";
452
+ *
453
+ * const dir = dataDir("my-cli");
454
+ * // → "/home/user/.local/share/my-cli" (Linux)
455
+ * // → "/Users/user/.local/share/my-cli" (macOS)
456
+ * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Data" (Windows)
270
457
  * ```
271
458
  */
272
- type InferStoreConfig<F extends FieldsDef> = { [K in keyof F] : InferFieldValue<F[K]> };
459
+ declare function dataDir(appName: string, env?: PlatformEnv): string;
273
460
  /**
274
- * Options for {@link createStore}.
461
+ * Resolves the platform-standard state directory for the given app.
275
462
  *
276
- * @typeParam F - The field definitions record (inferred via `const` generic).
463
+ * Platform conventions:
464
+ * - **Linux / macOS**: `$XDG_STATE_HOME/<appName>` or `~/.local/state/<appName>`
465
+ * - **Windows**: `%LOCALAPPDATA%/<appName>/State` or `~/AppData/Local/<appName>/State`
277
466
  *
278
- * @example
279
- * ```ts
280
- * const store = createStore({
281
- * dirPath: configDir("my-cli"),
282
- * fields: {
283
- * theme: { type: "string", default: "light" },
284
- * verbose: { type: "boolean", default: false },
285
- * },
286
- * });
287
- * ```
288
- */
289
- interface CreateStoreOptions<F extends FieldsDef> {
290
- /**
291
- * Absolute directory path where the store JSON file is persisted.
292
- *
293
- * Use the {@link configDir} helper to resolve the platform-standard
294
- * config directory from an app name.
295
- */
296
- dirPath: string;
297
- /**
298
- * Store name used as the JSON filename in the directory.
299
- *
300
- * Defaults to `"config"`, producing `config.json`. Set to a different value
301
- * to create multiple stores under the same directory
302
- * (e.g. `name: "auth"` → `auth.json`).
303
- *
304
- * Must not contain path separators or the `.json` extension.
305
- */
306
- name?: string;
307
- /**
308
- * Field definitions that declare the store's config schema.
309
- *
310
- * Each key maps to a {@link FieldDef} with a `type` discriminant and optional
311
- * `default`. Fields without a `default` are optional (`T | undefined`).
312
- */
313
- fields: F;
314
- }
315
- /**
316
- * Receives the current effective config and returns an updated config.
467
+ * macOS uses XDG conventions for consistency with Linux.
317
468
  *
318
- * Used by {@link Store.update} to apply partial mutations atomically.
469
+ * @param appName - Application name used as directory name. Must be a non-empty
470
+ * string without path separators.
471
+ * @param env - Optional platform environment override for testing.
472
+ * @returns Absolute path to the app's state directory.
473
+ * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
319
474
  *
320
475
  * @example
321
476
  * ```ts
322
- * await store.update((current) => ({
323
- * ...current,
324
- * theme: "dark",
325
- * }));
477
+ * import { stateDir } from "@crustjs/store";
478
+ *
479
+ * const dir = stateDir("my-cli");
480
+ * // → "/home/user/.local/state/my-cli" (Linux)
481
+ * // → "/Users/user/.local/state/my-cli" (macOS)
482
+ * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\State" (Windows)
326
483
  * ```
327
484
  */
328
- type StoreUpdater<TConfig> = (current: TConfig) => NoInfer<TConfig>;
485
+ declare function stateDir(appName: string, env?: PlatformEnv): string;
329
486
  /**
330
- * A typed async config store returned by {@link createStore}.
487
+ * Resolves the platform-standard cache directory for the given app.
331
488
  *
332
- * Provides `read`, `write`, `update`, and `reset` operations for a single
333
- * typed config object persisted as JSON on the local filesystem.
489
+ * Platform conventions:
490
+ * - **Linux / macOS**: `$XDG_CACHE_HOME/<appName>` or `~/.cache/<appName>`
491
+ * - **Windows**: `%LOCALAPPDATA%/<appName>/Cache` or `~/AppData/Local/<appName>/Cache`
334
492
  *
335
- * @typeParam TConfig - The inferred config shape from field definitions.
493
+ * macOS uses XDG conventions for consistency with Linux.
494
+ *
495
+ * @param appName - Application name used as directory name. Must be a non-empty
496
+ * string without path separators.
497
+ * @param env - Optional platform environment override for testing.
498
+ * @returns Absolute path to the app's cache directory.
499
+ * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
336
500
  *
337
501
  * @example
338
502
  * ```ts
339
- * const store = createStore({
340
- * dirPath: configDir("my-cli"),
341
- * fields: {
342
- * theme: { type: "string", default: "light" },
343
- * },
344
- * });
503
+ * import { cacheDir } from "@crustjs/store";
345
504
  *
346
- * const config = await store.read();
347
- * await store.write({ theme: "dark" });
348
- * await store.update((c) => ({ ...c, theme: "dark" }));
349
- * await store.reset();
505
+ * const dir = cacheDir("my-cli");
506
+ * // → "/home/user/.cache/my-cli" (Linux)
507
+ * // → "/Users/user/.cache/my-cli" (macOS)
508
+ * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Cache" (Windows)
350
509
  * ```
351
510
  */
352
- interface Store<TConfig> {
353
- /**
354
- * Reads the persisted config, applying field defaults for missing keys.
355
- *
356
- * Always returns a value — fields with defaults are guaranteed present,
357
- * fields without defaults may be `undefined`.
358
- *
359
- * @returns The effective config value.
360
- * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
361
- * @throws {CrustStoreError} `IO` on filesystem read failures.
362
- */
363
- read(): Promise<TConfig>;
364
- /**
365
- * Atomically persists the full config object.
366
- *
367
- * @param config - The complete config to persist.
368
- * @throws {CrustStoreError} `IO` on filesystem write failures.
369
- */
370
- write(config: NoInfer<TConfig>): Promise<void>;
371
- /**
372
- * Reads current effective config, applies the updater, and persists.
373
- *
374
- * @param updater - Function receiving current config and returning updated config.
375
- * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
376
- * @throws {CrustStoreError} `IO` on filesystem failures.
377
- */
378
- update(updater: StoreUpdater<TConfig>): Promise<void>;
379
- /**
380
- * Removes the persisted config file, returning the store to defaults-on-read behavior.
381
- *
382
- * @throws {CrustStoreError} `IO` on filesystem deletion failures.
383
- */
384
- reset(): Promise<void>;
385
- }
511
+ declare function cacheDir(appName: string, env?: PlatformEnv): string;
386
512
  /**
387
- * Creates a typed async config store backed by a local JSON file.
513
+ * Creates a typed async store backed by a local JSON file.
388
514
  *
389
515
  * The store resolves its file path once at creation time from `dirPath` and
390
- * optional `name`. Field definitions declare the config schema — each field's
391
- * `type` determines its TypeScript type, and the presence of `default`
392
- * determines whether the field is guaranteed or optional (`T | undefined`).
516
+ * optional `name`. The `defaults` object declares the data shape — its
517
+ * TypeScript type determines the store's type parameter `T`.
393
518
  *
394
- * @typeParam F - Field definitions record (inferred via `const` generic).
519
+ * @typeParam T - The store data shape (inferred from `defaults`).
395
520
  * @param options - Store configuration options.
396
- * @returns A {@link Store} instance with `read`, `write`, `update`, and `reset` methods.
521
+ * @returns A {@link Store} instance with `read`, `write`, `update`, `patch`, and `reset` methods.
397
522
  * @throws {CrustStoreError} `PATH` if `dirPath` or `name` is invalid.
398
523
  *
399
524
  * @example
@@ -402,20 +527,20 @@ interface Store<TConfig> {
402
527
  *
403
528
  * const store = createStore({
404
529
  * dirPath: configDir("my-cli"),
405
- * fields: {
406
- * theme: { type: "string", default: "light" },
407
- * verbose: { type: "boolean", default: false },
408
- * token: { type: "string" },
530
+ * defaults: {
531
+ * ui: { theme: "light", fontSize: 14 },
532
+ * verbose: false,
409
533
  * },
410
534
  * });
411
535
  *
412
- * const config = await store.read();
413
- * // → { theme: "light", verbose: false } (defaults when no persisted file)
536
+ * const state = await store.read();
537
+ * // → { ui: { theme: "light", fontSize: 14 }, verbose: false }
414
538
  *
415
- * await store.write({ theme: "dark", verbose: true, token: "abc" });
416
- * await store.update((c) => ({ ...c, theme: "light" }));
539
+ * await store.write({ ui: { theme: "dark", fontSize: 14 }, verbose: true });
540
+ * await store.update((s) => ({ ...s, verbose: false }));
541
+ * await store.patch({ ui: { theme: "dark" } });
417
542
  * await store.reset();
418
543
  * ```
419
544
  */
420
- declare function createStore<const F extends FieldsDef>(options: CreateStoreOptions<F>): Store<InferStoreConfig<F>>;
421
- export { createStore, configDir, ValueType, StoreUpdater, StoreErrorCode, Store, PlatformEnv, InferStoreConfig, FieldsDef, FieldDef, CrustStoreError, CreateStoreOptions };
545
+ declare function createStore<const T extends Record<string, unknown>>(options: CreateStoreOptions<T>): Store<T>;
546
+ export { stateDir, dataDir, createStore, configDir, cacheDir, ValidationErrorDetails, StoreValidatorSuccess, StoreValidatorResult, StoreValidatorIssue, StoreValidatorFailure, StoreValidator, StoreUpdater, StoreErrorCode, Store, PlatformEnv, CrustStoreError, CreateStoreOptions };