@crustjs/store 0.2.0 → 0.3.1

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,794 +1,701 @@
1
- import { BaseValueType, ResolvePrimitive } from "@crustjs/utils";
1
+ //#region ../utils/src/json.d.ts
2
+ /** Any value representable in a JSON document. */
3
+ type JsonValue = string | number | boolean | null | readonly JsonValue[] | JsonObject;
4
+ interface JsonObject {
5
+ [key: string]: JsonValue;
6
+ }
7
+ /** Preserve a type when all of its properties are recursively JSON-compatible. */
8
+ type JsonCompatible<T> = T extends JsonValue ? T : T extends ((...arguments_: never[]) => void) ? never : T extends readonly unknown[] ? { [K in keyof T]: JsonCompatible<T[K]>; } : T extends object ? { [K in keyof T]: JsonCompatible<T[K]>; } : never;
9
+ //#endregion
10
+ //#region ../utils/src/primitive.d.ts
11
+ /** Supported primitive type literals shared by Crust packages. */
12
+ type BaseValueType = "string" | "number" | "boolean";
13
+ /**
14
+ * Resolves a primitive type literal to its corresponding TypeScript type.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * type Port = ResolvePrimitive<"number">;
19
+ * // ^? number
20
+ * ```
21
+ */
22
+ type ResolvePrimitive<T extends BaseValueType> = T extends "string" ? string : T extends "number" ? number : T extends "boolean" ? boolean : never;
23
+ //#endregion
24
+ //#region ../utils/src/schema.d.ts
25
+ /** The Standard Typed types interface. */
26
+ interface StandardSchemaTypes<Input = unknown, Output = Input> {
27
+ /** The input type of the schema. */
28
+ readonly input: Input;
29
+ /** The output type of the schema. */
30
+ readonly output: Output;
31
+ }
32
+ /** The result interface if validation succeeds. */
33
+ interface StandardSchemaSuccessResult<Output> {
34
+ /** The typed output value. */
35
+ readonly value: Output;
36
+ /** A falsy value for `issues` indicates success. */
37
+ readonly issues?: undefined;
38
+ }
39
+ /** The result interface if validation fails. */
40
+ interface StandardSchemaFailureResult {
41
+ /** The issues of failed validation. */
42
+ readonly issues: ReadonlyArray<StandardSchemaIssue>;
43
+ }
44
+ /** The result interface of the validate function. */
45
+ type StandardSchemaResult<Output> = StandardSchemaSuccessResult<Output> | StandardSchemaFailureResult;
46
+ /** The issue interface of the failure output. */
47
+ interface StandardSchemaIssue {
48
+ /** The error message of the issue. */
49
+ readonly message: string;
50
+ /** The path of the issue, if any. */
51
+ readonly path?: ReadonlyArray<PropertyKey | StandardSchemaPathSegment> | undefined;
52
+ }
53
+ /** The path segment interface of the issue. */
54
+ interface StandardSchemaPathSegment {
55
+ /** The key representing a path segment. */
56
+ readonly key: PropertyKey;
57
+ }
58
+ /** The Standard Schema properties interface. */
59
+ interface StandardSchemaProps<Input = unknown, Output = Input> {
60
+ /** The version number of the standard. */
61
+ readonly version: 1;
62
+ /** The vendor name of the schema library. */
63
+ readonly vendor: string;
64
+ /** Inferred types associated with the schema. */
65
+ readonly types?: StandardSchemaTypes<Input, Output> | undefined;
66
+ /** Validates unknown input values. */
67
+ readonly validate: (value: StandardSchemaTypes["input"]) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
68
+ }
69
+ /**
70
+ * A Standard Schema-compatible schema object.
71
+ *
72
+ * Any schema library implementing the Standard Schema v1 spec
73
+ * (Zod, Effect, Valibot, ArkType, etc.) produces objects matching this type.
74
+ */
75
+ type StandardSchema<Input = unknown, Output = Input> = {
76
+ /** The Standard Schema properties. */
77
+ readonly "~standard": StandardSchemaProps<Input, Output>;
78
+ };
79
+ /** Infer the output type produced by a Standard Schema on success. */
80
+ type InferOutput<S extends StandardSchema> = NonNullable<S["~standard"]["types"]>["output"];
81
+ //#endregion
82
+ //#region src/types.d.ts
2
83
  /** Supported type literals for store fields. */
3
84
  type ValueType = BaseValueType;
4
85
  /** Shared fields present on every store field definition. */
5
86
  interface FieldDefBase<V> {
6
- /** Human-readable description for documentation and tooling. */
7
- description?: string;
8
- /**
9
- * Optional per-field validation function.
10
- *
11
- * Called during `read`, `write`, `update`, and `patch` operations when
12
- * the field has a value (not `undefined`).
13
- *
14
- * Return shapes:
15
- * - `void` (or `Promise<void>`) — validation-only; the input value is
16
- * persisted as-is. This is the contract for hand-rolled validators.
17
- * - `{ value }` (or `Promise<{ value }>`) — the schema (or transform)
18
- * produced an output value (typed as `unknown` because schema
19
- * transforms may emit a value of a different runtime type than
20
- * `V`; the read-stability guard enforces shape correctness at
21
- * write time). On `write` / `update` / `patch`, the transformed
22
- * value replaces the input before persistence and is re-validated
23
- * once to catch read-unstable transforms (cross-type transforms
24
- * whose output would fail the schema on the next read). On `read`,
25
- * the transformed value is discarded — reads always return the
26
- * on-disk value verbatim.
27
- * - Throwing an error — the value is rejected; the error message is
28
- * captured as a validation issue with the field name as `path`.
29
- *
30
- * @param value - The field value to validate.
31
- */
32
- validate?: (value: V) => void | Promise<void> | {
33
- value: unknown;
34
- } | Promise<{
35
- value: unknown;
36
- }>;
37
- /** Internal marker: validate missing values as `undefined` for schema-backed fields. */
38
- validateMissing?: true;
87
+ /** Standard Schemas are an exclusive definition mode. */
88
+ schema?: never;
89
+ /**
90
+ * Optional per-field validation function.
91
+ *
92
+ * Called during `read`, `write`, `update`, and `patch` operations when
93
+ * the field has a value (not `undefined`).
94
+ *
95
+ * Return shapes:
96
+ * - `void` (or `Promise<void>`) — validation-only; the input value is
97
+ * persisted as-is.
98
+ * - `{ value }` (or `Promise<{ value }>`) — a transform produced an
99
+ * output within the field's declared type. On `write` / `update` /
100
+ * `patch`, the transformed value replaces the input before
101
+ * persistence and is re-validated once to catch read-unstable
102
+ * transforms. Outputs outside the declared type are rejected with
103
+ * `VALIDATION`. On `read`, the transformed value is discarded —
104
+ * reads always return the on-disk value verbatim.
105
+ * - Throwing an error — the value is rejected; the error message is
106
+ * captured as a validation issue with the field name as `path`.
107
+ *
108
+ * @param value - The field value to validate.
109
+ */
110
+ validate?: (value: V) => void | Promise<void> | {
111
+ value: V;
112
+ } | Promise<{
113
+ value: V;
114
+ }>;
39
115
  }
40
116
  /** Base for scalar (non-array) fields — `array` must be omitted. */
41
117
  interface ScalarFieldBase<V> extends FieldDefBase<V> {
42
- /** Must be omitted for scalar fields — set to `true` for array fields. */
43
- array?: never;
118
+ /** Must be omitted for scalar fields — set to `true` for array fields. */
119
+ array?: never;
44
120
  }
45
121
  /** A scalar string field. */
46
122
  interface StringFieldDef extends ScalarFieldBase<string> {
47
- type: "string";
48
- /** Default string value when the field is not persisted. */
49
- default?: string;
123
+ type: "string";
124
+ /** Default string value when the field is not persisted. */
125
+ default?: string;
50
126
  }
51
127
  /** A scalar number field. */
52
128
  interface NumberFieldDef extends ScalarFieldBase<number> {
53
- type: "number";
54
- /** Default number value when the field is not persisted. */
55
- default?: number;
129
+ type: "number";
130
+ /** Default number value when the field is not persisted. */
131
+ default?: number;
56
132
  }
57
133
  /** A scalar boolean field. */
58
134
  interface BooleanFieldDef extends ScalarFieldBase<boolean> {
59
- type: "boolean";
60
- /** Default boolean value when the field is not persisted. */
61
- default?: boolean;
135
+ type: "boolean";
136
+ /** Default boolean value when the field is not persisted. */
137
+ default?: boolean;
62
138
  }
63
139
  /** Base for array fields — `array` is required as `true`. */
64
140
  interface ArrayFieldBase<V> extends FieldDefBase<V> {
65
- /** Collect values into an array. */
66
- array: true;
141
+ /** Collect values into an array. */
142
+ array: true;
67
143
  }
68
144
  /** An array of strings field. */
69
145
  interface StringArrayFieldDef extends ArrayFieldBase<string[]> {
70
- type: "string";
71
- /** Default string array value when the field is not persisted. */
72
- default?: readonly string[];
146
+ type: "string";
147
+ /** Default string array value when the field is not persisted. */
148
+ default?: readonly string[];
73
149
  }
74
150
  /** An array of numbers field. */
75
151
  interface NumberArrayFieldDef extends ArrayFieldBase<number[]> {
76
- type: "number";
77
- /** Default number array value when the field is not persisted. */
78
- default?: readonly number[];
152
+ type: "number";
153
+ /** Default number array value when the field is not persisted. */
154
+ default?: readonly number[];
79
155
  }
80
156
  /** An array of booleans field. */
81
157
  interface BooleanArrayFieldDef extends ArrayFieldBase<boolean[]> {
82
- type: "boolean";
83
- /** Default boolean array value when the field is not persisted. */
84
- default?: readonly boolean[];
158
+ type: "boolean";
159
+ /** Default boolean array value when the field is not persisted. */
160
+ default?: readonly boolean[];
85
161
  }
86
- /**
87
- * Defines a single field in a store's config schema.
88
- *
89
- * Discriminated by `type` and `array` for type-safe `default` values.
90
- * Each field can optionally include a `validate` function for per-field
91
- * validation and a `default` value for fallback behavior.
92
- *
93
- * @example
94
- * ```ts
95
- * const fields = {
96
- * theme: { type: "string", default: "light" },
97
- * verbose: { type: "boolean", default: false },
98
- * tags: { type: "string", array: true, default: [] },
99
- * token: { type: "string" }, // optional — no default
100
- * port: {
101
- * type: "number",
102
- * default: 3000,
103
- * validate: (v) => { if (v < 1 || v > 65535) throw new Error("invalid port"); },
104
- * },
105
- * } satisfies FieldsDef;
106
- * ```
107
- */
108
- interface RawScalarFieldDef extends ScalarFieldBase<unknown> {
109
- type?: never;
110
- default?: unknown;
111
- }
112
- interface RawArrayFieldDef extends ArrayFieldBase<unknown[]> {
113
- type?: never;
114
- default?: readonly unknown[];
162
+ /** A field whose Standard Schema exclusively owns its value semantics. */
163
+ interface SchemaFieldDef {
164
+ /** Standard Schema that owns validation, transformation, defaults, and optionality. */
165
+ schema: StandardSchema<unknown, unknown>;
166
+ type?: BaseValueType;
167
+ array?: true;
168
+ default?: never;
169
+ validate?: never;
115
170
  }
116
- type FieldDef = StringFieldDef | NumberFieldDef | BooleanFieldDef | StringArrayFieldDef | NumberArrayFieldDef | BooleanArrayFieldDef | RawScalarFieldDef | RawArrayFieldDef;
171
+ /**
172
+ * Defines a single field in a store's config schema.
173
+ *
174
+ * Core definitions are discriminated by `type` and `array`. Schema-backed
175
+ * definitions use a `schema` key and cannot mix schema semantics with
176
+ * `default` or `validate`.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * const fields = {
181
+ * theme: { type: "string", default: "light" },
182
+ * port: { schema: z.coerce.number().int().positive() },
183
+ * } satisfies FieldsDef;
184
+ * ```
185
+ */
186
+ type FieldDef = StringFieldDef | NumberFieldDef | BooleanFieldDef | StringArrayFieldDef | NumberArrayFieldDef | BooleanArrayFieldDef | SchemaFieldDef;
117
187
  /** Record mapping field names to their definitions. */
118
188
  type FieldsDef = Record<string, FieldDef>;
119
- declare const FIELD_SCHEMA_OUTPUT: unique symbol;
189
+ type JsonCompatibleFields<F extends FieldsDef> = { [K in keyof F]: F[K] extends {
190
+ schema: StandardSchema<unknown, infer Output>;
191
+ } ? Exclude<Output, undefined> extends JsonCompatible<Exclude<Output, undefined>> ? F[K] : never : F[K]; };
120
192
  /**
121
- * Infer the resolved type for a single field definition.
122
- *
123
- * - **array** → `primitive[]` (or `primitive[] | undefined` if no default)
124
- * - **has default** → `primitive` (guaranteed present)
125
- * - **no default** → `primitive | undefined` (optional)
126
- */
193
+ * Infer the resolved type for a single field definition.
194
+ *
195
+ * - **schema** → the schema's exact output type
196
+ * - **array** → `primitive[]` (or `primitive[] | undefined` if no default)
197
+ * - **has default** → `primitive` (guaranteed present)
198
+ * - **no default** → `primitive | undefined` (optional)
199
+ */
127
200
  type InferFieldValue<F extends FieldDef> = F extends {
128
- readonly [FIELD_SCHEMA_OUTPUT]?: infer O;
201
+ schema: infer S extends StandardSchema;
202
+ } ? InferOutput<S> : F extends {
203
+ type: ValueType;
129
204
  } ? F extends {
130
- default: infer D;
131
- } ? D : O | undefined : F extends {
132
- type: ValueType;
205
+ array: true;
133
206
  } ? F extends {
134
- array: true;
135
- } ? F extends {
136
- default: readonly ResolvePrimitive<F["type"]>[];
207
+ default: readonly ResolvePrimitive<F["type"]>[];
137
208
  } ? ResolvePrimitive<F["type"]>[] : ResolvePrimitive<F["type"]>[] | undefined : F extends {
138
- default: ResolvePrimitive<F["type"]>;
139
- } ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined : F extends {
140
- default: infer D;
141
- } ? D : unknown;
209
+ default: ResolvePrimitive<F["type"]>;
210
+ } ? ResolvePrimitive<F["type"]> : ResolvePrimitive<F["type"]> | undefined : never;
142
211
  /**
143
- * Maps a full {@link FieldsDef} record to the inferred config object type.
144
- *
145
- * @example
146
- * ```ts
147
- * type Config = InferStoreConfig<{
148
- * theme: { type: "string"; default: "light" };
149
- * verbose: { type: "boolean" };
150
- * }>;
151
- * // → { theme: string; verbose: boolean | undefined }
152
- * ```
153
- */
154
- type InferStoreConfig<F extends FieldsDef> = { [K in keyof F] : InferFieldValue<F[K]> };
212
+ * Maps a full {@link FieldsDef} record to the inferred config object type.
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * type Config = InferStoreConfig<{
217
+ * theme: { type: "string"; default: "light" };
218
+ * verbose: { type: "boolean" };
219
+ * }>;
220
+ * // → { theme: string; verbose: boolean | undefined }
221
+ * ```
222
+ */
223
+ type InferStoreConfig<F extends FieldsDef> = { [K in keyof F]: InferFieldValue<F[K]>; };
155
224
  /**
156
- * A single validation issue reported during store operations.
157
- */
225
+ * A single validation issue reported during store operations.
226
+ */
158
227
  interface StoreValidatorIssue {
159
- /** Human-readable description of the validation failure. */
160
- message: string;
161
- /** Field name where the issue occurred. */
162
- path: string;
228
+ /** Human-readable description of the validation failure. */
229
+ message: string;
230
+ /** Field name where the issue occurred. */
231
+ path: string;
163
232
  }
164
233
  /** Explicit Unix permission bits for store persistence. */
165
234
  interface StorePermissionBits {
166
- /** Permission bits for the persisted store file (e.g. `0o600`). */
167
- file?: number;
168
- /** Permission bits for the store's parent directory (e.g. `0o700`). */
169
- directory?: number;
235
+ /** Permission bits for the persisted store file (e.g. `0o600`). */
236
+ file?: number;
237
+ /** Permission bits for the store's parent directory (e.g. `0o700`). */
238
+ directory?: number;
170
239
  }
171
240
  /**
172
- * Persistence visibility for store writes.
173
- *
174
- * - `"default"` or omitted uses platform defaults.
175
- * - `"private"` keeps the store owner-only on Unix (`0600` file, `0700` directory).
176
- * - An object lets advanced callers provide explicit Unix permission bits for
177
- * group-readable or public non-secret stores without expanding the preset list.
178
- */
241
+ * Persistence visibility for store writes.
242
+ *
243
+ * - `"default"` or omitted uses platform defaults.
244
+ * - `"private"` keeps the store owner-only on Unix (`0600` file, `0700` directory).
245
+ * - An object lets advanced callers provide explicit Unix permission bits for
246
+ * group-readable or public non-secret stores without expanding the preset list.
247
+ */
179
248
  type StoreAccess = "default" | "private" | StorePermissionBits;
180
249
  /**
181
- * Options for {@link createStore}.
182
- *
183
- * @typeParam F - The field definitions record (inferred via `const` generic).
184
- *
185
- * @example
186
- * ```ts
187
- * const store = createStore({
188
- * dirPath: configDir("my-cli"),
189
- * fields: {
190
- * theme: { type: "string", default: "light" },
191
- * verbose: { type: "boolean", default: false },
192
- * token: { type: "string" },
193
- * },
194
- * });
195
- * ```
196
- */
250
+ * Options for {@link createStore}.
251
+ *
252
+ * @typeParam F - The field definitions record (inferred via `const` generic).
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * const store = createStore({
257
+ * dirPath: configDir("my-cli"),
258
+ * name: "config",
259
+ * fields: {
260
+ * theme: { type: "string", default: "light" },
261
+ * verbose: { type: "boolean", default: false },
262
+ * token: { type: "string" },
263
+ * },
264
+ * });
265
+ * ```
266
+ */
197
267
  interface CreateStoreOptions<F extends FieldsDef> {
198
- /**
199
- * Absolute directory path where the store JSON file is persisted.
200
- *
201
- * Use a path helper ({@link configDir}, {@link dataDir}, etc.) to resolve
202
- * the platform-standard directory from an app name.
203
- */
204
- dirPath: string;
205
- /**
206
- * Store name used as the JSON filename in the directory.
207
- *
208
- * Defaults to `"config"`, producing `config.json`. Set to a different value
209
- * to create multiple stores under the same directory
210
- * (e.g. `name: "auth"` → `auth.json`).
211
- *
212
- * Must not contain path separators or the `.json` extension.
213
- */
214
- name?: string;
215
- /**
216
- * Field definitions that declare the store's config schema.
217
- *
218
- * Each key maps to a {@link FieldDef} with a `type` discriminant and optional
219
- * `default` and `validate`. Fields without a `default` are optional (`T | undefined`).
220
- */
221
- fields: F;
222
- /**
223
- * When `true` (the default), persisted keys not present in `fields`
224
- * are dropped on read. Set to `false` to preserve unknown keys.
225
- *
226
- * @default true
227
- */
228
- pruneUnknown?: boolean;
229
- /**
230
- * Persistence visibility for store writes.
231
- *
232
- * Use `"private"` for config/state stores holding secrets (tokens, API keys)
233
- * that should stay owner-only. On Unix this writes the file as `0600` and
234
- * creates the parent directory as `0700`; on Windows it is not enforced
235
- * because Windows uses ACLs instead of Unix permission bits.
236
- *
237
- * The only built-in presets are `"default"` and `"private"`. Advanced callers
238
- * can pass explicit bits with `{ file, directory }` for group-readable or public
239
- * non-secret stores. The file bits are enforced exactly on every write
240
- * regardless of `umask`. Directory bits apply only when a write creates the
241
- * directory; pre-existing directories are left untouched.
242
- *
243
- * @default "default"
244
- * @example "private"
245
- * @example { file: 0o600, directory: 0o700 }
246
- */
247
- access?: StoreAccess;
268
+ /**
269
+ * Absolute directory path where the store JSON file is persisted.
270
+ *
271
+ * Use a path helper ({@link configDir}, {@link dataDir}, etc.) to resolve
272
+ * the platform-standard directory from an app name.
273
+ */
274
+ dirPath: string;
275
+ /**
276
+ * Store name used as the JSON filename in the directory.
277
+ *
278
+ * For example, `name: "auth"` produces `auth.json`. Must not contain path
279
+ * separators or the `.json` extension.
280
+ */
281
+ name: string;
282
+ /**
283
+ * Field definitions that declare the store's config schema.
284
+ *
285
+ * Each key maps to a {@link FieldDef}: either a core definition or a
286
+ * schema-backed definition. Core fields without `default` are optional;
287
+ * schema fields use the schema's exact output type.
288
+ */
289
+ fields: F & JsonCompatibleFields<F>;
290
+ /**
291
+ * When `true` (the default), persisted keys not present in `fields`
292
+ * are dropped on read. Set to `false` to preserve unknown keys.
293
+ *
294
+ * @default true
295
+ */
296
+ pruneUnknown?: boolean;
297
+ /**
298
+ * Persistence visibility for store writes.
299
+ *
300
+ * Use `"private"` for config/state stores holding secrets (tokens, API keys)
301
+ * that should stay owner-only. On Unix this writes the file as `0600` and
302
+ * creates the parent directory as `0700`; on Windows it is not enforced
303
+ * because Windows uses ACLs instead of Unix permission bits.
304
+ *
305
+ * The only built-in presets are `"default"` and `"private"`. Advanced callers
306
+ * can pass explicit bits with `{ file, directory }` for group-readable or public
307
+ * non-secret stores. The file bits are enforced exactly on every write
308
+ * regardless of `umask`. Directory bits apply only when a write creates the
309
+ * directory; pre-existing directories are left untouched.
310
+ *
311
+ * @default "default"
312
+ * @example "private"
313
+ * @example { file: 0o600, directory: 0o700 }
314
+ */
315
+ access?: StoreAccess;
248
316
  }
249
317
  /**
250
- * Receives the current effective config and returns an updated config.
251
- *
252
- * Used by {@link Store.update} to apply mutations atomically.
253
- *
254
- * @example
255
- * ```ts
256
- * await store.update((current) => ({
257
- * ...current,
258
- * theme: "dark",
259
- * }));
260
- * ```
261
- */
318
+ * Receives the current config with defaults and core coercion applied,
319
+ * but without full field validation, and returns an updated config.
320
+ * Persisted values may not yet satisfy their declared types or validators.
321
+ *
322
+ * Used by {@link Store.update}, which validates the result before persistence.
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * await store.update((current) => ({
327
+ * ...current,
328
+ * theme: "dark",
329
+ * }));
330
+ * ```
331
+ */
262
332
  type StoreUpdater<TConfig> = (current: TConfig) => NoInfer<TConfig>;
263
333
  /**
264
- * A typed async config store returned by {@link createStore}.
265
- *
266
- * Provides `read`, `write`, `update`, `patch`, and `reset` operations for a
267
- * single typed config object persisted as JSON on the local filesystem.
268
- *
269
- * @typeParam TConfig - The inferred config shape from field definitions.
270
- *
271
- * @example
272
- * ```ts
273
- * const store = createStore({
274
- * dirPath: configDir("my-cli"),
275
- * fields: {
276
- * theme: { type: "string", default: "light" },
277
- * verbose: { type: "boolean", default: false },
278
- * },
279
- * });
280
- *
281
- * const config = await store.read();
282
- * await store.write({ theme: "dark", verbose: true });
283
- * await store.update((c) => ({ ...c, theme: "dark" }));
284
- * await store.patch({ theme: "dark" });
285
- * await store.reset();
286
- * ```
287
- */
334
+ * A typed async config store returned by {@link createStore}.
335
+ *
336
+ * Provides `read`, `write`, `update`, `patch`, and `reset` operations for a
337
+ * single typed config object persisted as JSON on the local filesystem.
338
+ * Mutations are not serialized, including overlapping calls in one process.
339
+ * Await mutations sequentially for each file; coordinate multiple processes externally.
340
+ *
341
+ * @typeParam TConfig - The inferred config shape from field definitions.
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * const store = createStore({
346
+ * dirPath: configDir("my-cli"),
347
+ * name: "config",
348
+ * fields: {
349
+ * theme: { type: "string", default: "light" },
350
+ * verbose: { type: "boolean", default: false },
351
+ * },
352
+ * });
353
+ *
354
+ * const config = await store.read();
355
+ * await store.write({ theme: "dark", verbose: true });
356
+ * await store.update((c) => ({ ...c, theme: "dark" }));
357
+ * await store.patch({ theme: "dark" });
358
+ * await store.reset();
359
+ * ```
360
+ */
288
361
  interface Store<TConfig> {
289
- /**
290
- * Reads the persisted config, applying field defaults for missing keys.
291
- *
292
- * Always returns a value — fields with defaults are guaranteed present,
293
- * fields without defaults may be `undefined`.
294
- *
295
- * When fields have `validate` functions, the merged config is validated
296
- * after defaults are applied. Invalid persisted config fails loudly.
297
- *
298
- * @returns The effective config value.
299
- * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
300
- * @throws {CrustStoreError} `VALIDATION` if field validation fails.
301
- * @throws {CrustStoreError} `IO` on filesystem read failures.
302
- */
303
- read(): Promise<TConfig>;
304
- /**
305
- * Atomically persists the full config object.
306
- *
307
- * When fields have `validate` functions, the config is validated before
308
- * persistence.
309
- *
310
- * @param config - The complete config to persist.
311
- * @throws {CrustStoreError} `VALIDATION` if field validation fails.
312
- * @throws {CrustStoreError} `IO` on filesystem write failures.
313
- */
314
- write(config: NoInfer<TConfig>): Promise<void>;
315
- /**
316
- * Reads current effective config, applies the updater, and persists.
317
- *
318
- * @param updater - Function receiving current config and returning updated config.
319
- * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
320
- * @throws {CrustStoreError} `VALIDATION` if field validation fails.
321
- * @throws {CrustStoreError} `IO` on filesystem failures.
322
- */
323
- update(updater: StoreUpdater<TConfig>): Promise<void>;
324
- /**
325
- * Applies a partial update to the current config and persists.
326
- *
327
- * Only the provided keys are updated; everything else is preserved.
328
- *
329
- * @param partial - A partial subset of the config to merge in.
330
- * @throws {CrustStoreError} `VALIDATION` if field validation fails.
331
- * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
332
- * @throws {CrustStoreError} `IO` on filesystem failures.
333
- */
334
- patch(partial: Partial<NoInfer<TConfig>>): Promise<void>;
335
- /**
336
- * Removes the persisted config file, returning the store to
337
- * defaults-on-read behavior.
338
- *
339
- * @throws {CrustStoreError} `IO` on filesystem deletion failures.
340
- */
341
- reset(): Promise<void>;
342
- }
343
- /**
344
- * Contextual details attached to a `PATH` error.
345
- *
346
- * Returned when the resolved or provided config file path is invalid.
347
- */
348
- interface PathErrorDetails {
349
- /** The path value that caused the error. */
350
- path: string;
362
+ /**
363
+ * Reads the persisted config, applying field defaults for missing keys.
364
+ *
365
+ * Always returns a value — fields with defaults are guaranteed present,
366
+ * fields without defaults may be `undefined`.
367
+ *
368
+ * When fields have `validate` functions, the merged config is validated
369
+ * after defaults are applied. Invalid persisted config fails loudly.
370
+ *
371
+ * @returns The effective config value.
372
+ * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed or its root is not an object.
373
+ * @throws {CrustStoreError} `VALIDATION` if field validation fails.
374
+ * @throws {CrustStoreError} `IO` on filesystem read failures.
375
+ */
376
+ read(): Promise<TConfig>;
377
+ /**
378
+ * Atomically persists the full config object.
379
+ *
380
+ * When fields have `validate` functions, the config is validated before
381
+ * persistence.
382
+ *
383
+ * @param config - The complete config to persist.
384
+ * @returns The persisted config, including schema transformations.
385
+ * @throws {CrustStoreError} `VALIDATION` if field validation fails.
386
+ * @throws {CrustStoreError} `IO` on filesystem write failures.
387
+ */
388
+ write(config: NoInfer<TConfig>): Promise<TConfig>;
389
+ /**
390
+ * Reads current config with defaults and core coercion applied, without
391
+ * full field validation. Applies the updater, validates the result, and persists.
392
+ *
393
+ * The final file replacement is atomic, but the read-modify-write sequence
394
+ * is not serialized, even within one process. Await mutations sequentially
395
+ * to avoid lost updates; coordinate multiple processes externally.
396
+ *
397
+ * @param updater - Function receiving current config and returning updated config.
398
+ * @returns The persisted config, including schema transformations.
399
+ * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed or its root is not an object.
400
+ * @throws {CrustStoreError} `VALIDATION` if field validation fails.
401
+ * @throws {CrustStoreError} `IO` on filesystem failures.
402
+ */
403
+ update(updater: StoreUpdater<TConfig>): Promise<TConfig>;
404
+ /**
405
+ * Applies a partial update to the current config and persists.
406
+ *
407
+ * Only the provided keys are updated; everything else is preserved.
408
+ * Like {@link Store.update}, the current config has defaults and core coercion
409
+ * applied but has not undergone full field validation. The merged result is
410
+ * validated before persistence. Await mutations sequentially to avoid lost updates.
411
+ *
412
+ * @param partial - A partial subset of the config to merge in.
413
+ * @returns The persisted config, including schema transformations.
414
+ * @throws {CrustStoreError} `VALIDATION` if field validation fails.
415
+ * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed or its root is not an object.
416
+ * @throws {CrustStoreError} `IO` on filesystem failures.
417
+ */
418
+ patch(partial: Partial<NoInfer<TConfig>>): Promise<TConfig>;
419
+ /**
420
+ * Removes the persisted config file, returning the store to
421
+ * defaults-on-read behavior.
422
+ *
423
+ * @throws {CrustStoreError} `IO` on filesystem deletion failures.
424
+ */
425
+ reset(): Promise<void>;
351
426
  }
427
+ //#endregion
428
+ //#region src/errors.d.ts
352
429
  /**
353
- * Contextual details attached to a `PARSE` error.
354
- *
355
- * Returned when the persisted JSON file cannot be parsed.
356
- */
357
- interface ParseErrorDetails {
358
- /** Absolute path to the file that failed parsing. */
359
- path: string;
360
- }
361
- /**
362
- * Contextual details attached to an `IO` error.
363
- *
364
- * Returned on filesystem read, write, or delete failures.
365
- */
430
+ * Contextual details attached to an `IO` error.
431
+ *
432
+ * Returned on filesystem read, write, or delete failures.
433
+ */
366
434
  interface IOErrorDetails {
367
- /** Absolute path to the file involved in the failed operation. */
368
- path: string;
369
- /** The filesystem operation that failed (e.g. `"read"`, `"write"`, `"delete"`). */
370
- operation: "read" | "write" | "delete";
435
+ /** Absolute path to the file involved in the failed operation. */
436
+ path: string;
437
+ /** The filesystem operation that failed (e.g. `"read"`, `"write"`, `"delete"`). */
438
+ operation: "read" | "write" | "delete";
371
439
  }
372
440
  /**
373
- * Contextual details attached to a `VALIDATION` error.
374
- *
375
- * Returned when a store validator rejects a config object during
376
- * read, write, update, or patch operations.
377
- */
441
+ * Contextual details attached to a `VALIDATION` error.
442
+ *
443
+ * Returned when a store validator rejects a config object during
444
+ * read, write, update, or patch operations.
445
+ */
378
446
  interface ValidationErrorDetails {
379
- /** The store operation during which validation failed. */
380
- operation: "read" | "write" | "update" | "patch";
381
- /** Structured validation issues for programmatic handling. */
382
- issues: readonly StoreValidatorIssue[];
383
- }
384
- /**
385
- * Contextual details attached to a `DEFINITION` error.
386
- *
387
- * Returned when a `field()` call cannot produce a valid `FieldDef` from the
388
- * provided schema and options — e.g. the input is not a Standard Schema v1
389
- * object.
390
- */
391
- interface DefinitionErrorDetails {
392
- /** The schema vendor name, when available. */
393
- vendor?: string;
447
+ /** The store operation during which validation failed. */
448
+ operation: "read" | "write" | "update" | "patch";
449
+ /** Structured validation issues for programmatic handling. */
450
+ issues: readonly StoreValidatorIssue[];
394
451
  }
395
452
  /**
396
- * Maps each {@link StoreErrorCode} to its structured details type.
397
- *
398
- * Used internally for conditional constructor parameters and type narrowing.
399
- */
453
+ * Maps each {@link StoreErrorCode} to its structured details type.
454
+ *
455
+ * Used internally for conditional constructor parameters and type narrowing.
456
+ */
400
457
  interface StoreErrorDetailsMap {
401
- PATH: PathErrorDetails;
402
- PARSE: ParseErrorDetails;
403
- IO: IOErrorDetails;
404
- VALIDATION: ValidationErrorDetails;
405
- DEFINITION: DefinitionErrorDetails;
458
+ PATH: {
459
+ path: string;
460
+ };
461
+ PARSE: {
462
+ path: string;
463
+ };
464
+ IO: IOErrorDetails;
465
+ VALIDATION: ValidationErrorDetails;
466
+ DEFINITION: undefined;
406
467
  }
407
468
  /**
408
- * All possible error codes emitted by `@crustjs/store`.
409
- *
410
- * - `PATH` — Invalid or unsupported config file path
411
- * - `PARSE` — Malformed JSON in persisted config file
412
- * - `IO` — Filesystem read, write, or delete failure
413
- * - `VALIDATION` — Config object rejected by a store validator
414
- * - `DEFINITION` — `field()` cannot produce a valid `FieldDef` from the given schema/options
415
- *
416
- * @example
417
- * ```ts
418
- * import { CrustStoreError } from "@crustjs/store";
419
- *
420
- * try {
421
- * const config = await store.read();
422
- * } catch (err) {
423
- * if (err instanceof CrustStoreError) {
424
- * switch (err.code) {
425
- * case "PARSE":
426
- * console.error(`Corrupt config at ${err.details.path}`);
427
- * break;
428
- * case "VALIDATION":
429
- * for (const issue of err.details.issues) {
430
- * console.error(`${issue.path}: ${issue.message}`);
431
- * }
432
- * break;
433
- * case "IO":
434
- * console.error(`File ${err.details.operation} failed: ${err.message}`);
435
- * break;
436
- * }
437
- * }
438
- * }
439
- * ```
440
- */
469
+ * All possible error codes emitted by `@crustjs/store`.
470
+ *
471
+ * - `PATH` — Invalid or unsupported config file path
472
+ * - `PARSE` — Malformed JSON in persisted config file
473
+ * - `IO` — Filesystem read, write, or delete failure
474
+ * - `VALIDATION` — Config object rejected by a store validator
475
+ * - `DEFINITION` — A field mixes mutually exclusive definition modes
476
+ *
477
+ * @example
478
+ * ```ts
479
+ * import { CrustStoreError } from "@crustjs/store";
480
+ *
481
+ * try {
482
+ * const config = await store.read();
483
+ * } catch (err) {
484
+ * if (err instanceof CrustStoreError && err.is("PARSE")) {
485
+ * console.error(`Corrupt config at ${err.details.path}`);
486
+ * } else if (err instanceof CrustStoreError && err.is("VALIDATION")) {
487
+ * for (const issue of err.details.issues) {
488
+ * console.error(`${issue.path}: ${issue.message}`);
489
+ * }
490
+ * } else if (err instanceof CrustStoreError && err.is("IO")) {
491
+ * console.error(`File ${err.details.operation} failed: ${err.message}`);
492
+ * }
493
+ * }
494
+ * ```
495
+ */
441
496
  type StoreErrorCode = keyof StoreErrorDetailsMap;
442
497
  /** Resolves the details type for a given {@link StoreErrorCode}. */
443
498
  type StoreErrorDetails<C extends StoreErrorCode> = StoreErrorDetailsMap[C];
444
499
  /**
445
- * A typed error thrown by `@crustjs/store` for path, parse, IO, and validation failures.
446
- *
447
- * Every `CrustStoreError` carries a {@link StoreErrorCode} that identifies the failure
448
- * category, along with structured {@link StoreErrorDetails} for programmatic handling
449
- * without fragile message parsing.
450
- *
451
- * @example
452
- * ```ts
453
- * import { CrustStoreError } from "@crustjs/store";
454
- *
455
- * try {
456
- * const config = await store.read();
457
- * } catch (err) {
458
- * if (err instanceof CrustStoreError && err.is("PARSE")) {
459
- * console.error(`Corrupt config at ${err.details.path}`);
460
- * }
461
- * }
462
- * ```
463
- */
464
- declare class CrustStoreError<C extends StoreErrorCode = StoreErrorCode> extends Error {
465
- /** Machine-readable error code for programmatic handling. */
466
- readonly code: C;
467
- /** Structured payload with context about the failure. */
468
- readonly details: StoreErrorDetails<C>;
469
- /** Optional wrapped original error or value. */
470
- cause?: unknown;
471
- constructor(code: C, message: string, details: StoreErrorDetails<C>);
472
- /**
473
- * Type-guard that narrows this error to a specific {@link StoreErrorCode}.
474
- *
475
- * When the guard returns `true`, TypeScript narrows the `details` property
476
- * to the corresponding details type.
477
- *
478
- * @example
479
- * ```ts
480
- * if (err.is("IO")) {
481
- * // err.details is IOErrorDetails
482
- * console.error(err.details.operation, err.details.path);
483
- * }
484
- * ```
485
- */
486
- is<T extends StoreErrorCode>(code: T): this is CrustStoreError<T>;
487
- /**
488
- * Attaches a cause to this error for stack-trace chaining.
489
- *
490
- * Returns `this` for fluent usage.
491
- *
492
- * @example
493
- * ```ts
494
- * throw new CrustStoreError("IO", "write failed", {
495
- * path: "/path/to/config.json",
496
- * operation: "write",
497
- * }).withCause(originalError);
498
- * ```
499
- */
500
- withCause(cause: unknown): this;
501
- }
502
- import { BaseValueType as BaseValueType2 } from "@crustjs/utils";
503
- import { InferOutput, StandardSchema } from "@crustjs/utils/schema";
504
- /**
505
- * Optional Crust metadata for the `FieldDef` shape produced by {@link field}.
506
- *
507
- * Every key is optional. Crust does not infer metadata from schemas; schemas
508
- * validate and transform actual runtime values, including missing values passed
509
- * as `undefined`.
510
- *
511
- * No `validate` key — validation flows exclusively through the schema.
512
- * If extra checks are needed, refine the schema with `.refine(...)`
513
- * (Zod), `Schema.filter(...)` (Effect), etc.
514
- *
515
- * @typeParam T - The schema's output value type. Used to type-tighten the
516
- * `default` key when the user wants a non-`undefined` field.
517
- */
518
- interface FieldOptions<T = unknown> {
519
- type?: BaseValueType2;
520
- description?: string;
521
- /**
522
- * Crust metadata default for this field when the persisted state does not
523
- * contain a value for it. Schema defaults should usually live in the schema;
524
- * missing persisted values are validated as `undefined`, so `.default()` works
525
- * naturally at read time.
526
- */
527
- default?: T;
528
- /** Mark this field as an array (collects values into an array). */
529
- array?: true;
500
+ * A typed error thrown by `@crustjs/store` for path, parse, IO, and validation failures.
501
+ *
502
+ * Every `CrustStoreError` carries a {@link StoreErrorCode} that identifies the failure
503
+ * category. Codes other than `DEFINITION` include structured {@link StoreErrorDetails}
504
+ * for programmatic handling without fragile message parsing.
505
+ *
506
+ * @example
507
+ * ```ts
508
+ * import { CrustStoreError } from "@crustjs/store";
509
+ *
510
+ * try {
511
+ * const config = await store.read();
512
+ * } catch (err) {
513
+ * if (err instanceof CrustStoreError && err.is("PARSE")) {
514
+ * console.error(`Corrupt config at ${err.details.path}`);
515
+ * }
516
+ * }
517
+ * ```
518
+ */
519
+ export declare class CrustStoreError<C extends StoreErrorCode = StoreErrorCode> extends Error {
520
+ /** Machine-readable error code for programmatic handling. */
521
+ readonly code: C;
522
+ /** Structured payload with context about the failure. */
523
+ readonly details: StoreErrorDetails<C>;
524
+ constructor(code: C, message: string, ...[details, cause]: StoreErrorDetails<C> extends undefined ? [details?: undefined, cause?: unknown] : [details: StoreErrorDetails<C>, cause?: unknown]);
525
+ /**
526
+ * Type-guard that narrows this error to a specific {@link StoreErrorCode}.
527
+ *
528
+ * When the guard returns `true`, TypeScript narrows the `details` property
529
+ * to the corresponding details type.
530
+ *
531
+ * @example
532
+ * ```ts
533
+ * if (err.is("IO")) {
534
+ * // err.details is IOErrorDetails
535
+ * console.error(err.details.operation, err.details.path);
536
+ * }
537
+ * ```
538
+ */
539
+ is<T extends StoreErrorCode>(code: T): this is CrustStoreError<T>;
530
540
  }
531
- type RawSchemaFieldDef<Out> = {
532
- readonly type?: never;
533
- readonly array?: never;
534
- readonly description?: string;
535
- readonly validate: (value: unknown) => Promise<{
536
- value: unknown;
537
- }>;
538
- readonly validateMissing: true;
539
- readonly [FIELD_SCHEMA_OUTPUT]?: Out;
540
- };
541
- type SchemaFieldDefWithOptions<
542
- Out,
543
- Type extends FieldOptions["type"],
544
- Array extends true | undefined,
545
- D = never
546
- > = Omit<RawSchemaFieldDef<Out>, "type" | "array"> & (Type extends NonNullable<FieldOptions["type"]> ? {
547
- readonly type: Type;
548
- } : {
549
- readonly type?: never;
550
- }) & (Array extends true ? {
551
- readonly array: true;
552
- } : {
553
- readonly array?: never;
554
- }) & ([D] extends [never] ? unknown : {
555
- readonly default: D;
556
- });
557
- /**
558
- * `FieldDef` inferred from a Standard Schema, with no narrowed default.
559
- *
560
- * Standard Schema v1 has no spec-portable type-level access to defaults, so
561
- * schema `.default()` does not narrow the inferred TypeScript type. Pass
562
- * `field(schema, { default: x })` explicitly to narrow.
563
- */
564
- type SchemaFieldDef<S extends StandardSchema> = RawSchemaFieldDef<InferOutput<S>>;
565
- /**
566
- * Define a `@crustjs/store` field from any Standard Schema v1.
567
- *
568
- * Returns a value that satisfies store's `FieldDef` discriminated union.
569
- * Crust stores raw values and lets the schema validate and transform them on
570
- * read/write; missing persisted values flow through the schema as `undefined`,
571
- * so `.optional()` and `.default()` behave naturally.
572
- *
573
- * The returned `validate` is an async function that throws an `Error` with
574
- * the schema's normalized issue messages on failure (matches store's
575
- * `FieldDef.validate` contract).
576
- *
577
- * **Type-level defaults**: Standard Schema v1 has no spec-portable type-level
578
- * access to defaults. For tight typing of default-bearing fields, pass
579
- * `default` via `opts`:
580
- *
581
- * ```ts
582
- * field(z.string().default("x")) // value materializes on read; state: string | undefined
583
- * field(z.string(), { default: "x" }) // Crust-level default; state: string
584
- * ```
585
- *
586
- * @param schema - Any Standard Schema v1 object (Zod schemas natively;
587
- * Effect schemas wrapped via `Schema.standardSchemaV1`;
588
- * Valibot/ArkType/Sury/etc. as-is)
589
- * @param opts - Optional Crust-level metadata
590
- * @throws {CrustStoreError} With code `"DEFINITION"` when the input is not
591
- * a Standard Schema v1 object.
592
- *
593
- * @example Zod
594
- * ```ts
595
- * import { z } from "zod";
596
- * import { createStore, configDir, field } from "@crustjs/store";
597
- *
598
- * const store = createStore({
599
- * dirPath: configDir("my-cli"),
600
- * fields: {
601
- * theme: field(z.enum(["light", "dark"]).default("light")),
602
- * verbose: field(z.boolean().default(false)),
603
- * tags: field(z.array(z.string()).default([])),
604
- * },
605
- * });
606
- * ```
607
- *
608
- * @example Effect
609
- * ```ts
610
- * import * as Schema from "effect/Schema";
611
- * import { createStore, configDir, field } from "@crustjs/store";
612
- *
613
- * const store = createStore({
614
- * dirPath: configDir("my-cli"),
615
- * fields: {
616
- * theme: field(Schema.standardSchemaV1(Schema.Literal("light", "dark"))),
617
- * },
618
- * });
619
- * ```
620
- */
621
- declare function field<S extends StandardSchema>(schema: S): SchemaFieldDef<S>;
622
- declare function field<
623
- S extends StandardSchema,
624
- D extends InferOutput<S>,
625
- const Type extends FieldOptions<InferOutput<S>>["type"] = undefined,
626
- const Array extends true | undefined = undefined
627
- >(schema: S, opts: FieldOptions<InferOutput<S>> & {
628
- default: D;
629
- type?: Type;
630
- array?: Array;
631
- }): SchemaFieldDefWithOptions<InferOutput<S>, Type, Array, D>;
632
- declare function field<
633
- S extends StandardSchema,
634
- const Type extends FieldOptions<InferOutput<S>>["type"] = undefined,
635
- const Array extends true | undefined = undefined
636
- >(schema: S, opts: FieldOptions<InferOutput<S>> & {
637
- type?: Type;
638
- array?: Array;
639
- }): SchemaFieldDefWithOptions<InferOutput<S>, Type, Array>;
541
+ //#endregion
542
+ //#region src/path.d.ts
640
543
  /**
641
- * Encapsulates runtime environment values needed for path resolution.
642
- *
643
- * Exposed as a parameter to allow deterministic testing without mutating
644
- * `process.env` or `process.platform`.
645
- */
544
+ * Encapsulates runtime environment values needed for path resolution.
545
+ *
546
+ * Exposed as a parameter to allow deterministic testing without mutating
547
+ * `process.env` or `process.platform`.
548
+ */
646
549
  interface PlatformEnv {
647
- /** Operating system platform identifier (e.g. `"linux"`, `"darwin"`, `"win32"`). */
648
- platform: string;
649
- /** Environment variables map. */
650
- env: Record<string, string | undefined>;
651
- /** User home directory path. */
652
- homedir: string;
550
+ /** Operating system platform identifier (e.g. `"linux"`, `"darwin"`, `"win32"`). */
551
+ platform: string;
552
+ /** Environment variables map. */
553
+ env: Record<string, string | undefined>;
554
+ /** User home directory path. */
555
+ homedir: string;
653
556
  }
654
557
  /**
655
- * Resolves the platform-standard config directory for the given app.
656
- *
657
- * Platform conventions:
658
- * - **Linux / macOS**: `$XDG_CONFIG_HOME/<appName>` or `~/.config/<appName>`
659
- * - **Windows**: `%APPDATA%/<appName>` or `~/AppData/Roaming/<appName>`
660
- *
661
- * macOS uses XDG conventions for consistency with Linux.
662
- *
663
- * @param appName - Application name used as directory name. Must be a non-empty
664
- * string without path separators.
665
- * @param env - Optional platform environment override for testing.
666
- * @returns Absolute path to the app's config directory.
667
- * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
668
- *
669
- * @example
670
- * ```ts
671
- * import { configDir } from "@crustjs/store";
672
- *
673
- * const dir = configDir("my-cli");
674
- * // → "/home/user/.config/my-cli" (Linux)
675
- * // → "/Users/user/.config/my-cli" (macOS)
676
- * // → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
677
- * ```
678
- */
679
- declare function configDir(appName: string, env?: PlatformEnv): string;
558
+ * Resolves the platform-standard config directory for the given app.
559
+ *
560
+ * Platform conventions:
561
+ * - **Linux / macOS**: `$XDG_CONFIG_HOME/<appName>` or `~/.config/<appName>`
562
+ * - **Windows**: `%APPDATA%/<appName>` or `~/AppData/Roaming/<appName>`
563
+ *
564
+ * macOS uses XDG conventions for consistency with Linux.
565
+ *
566
+ * @param appName - Application name used as directory name. Must be a non-empty
567
+ * string without path separators.
568
+ * @param env - Optional platform environment override for testing.
569
+ * @returns Absolute path to the app's config directory.
570
+ * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
571
+ *
572
+ * @example
573
+ * ```ts
574
+ * import { configDir } from "@crustjs/store";
575
+ *
576
+ * const dir = configDir("my-cli");
577
+ * // → "/home/user/.config/my-cli" (Linux)
578
+ * // → "/Users/user/.config/my-cli" (macOS)
579
+ * // → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
580
+ * ```
581
+ */
582
+ export declare function configDir(appName: string, env?: PlatformEnv): string;
680
583
  /**
681
- * Resolves the platform-standard data directory for the given app.
682
- *
683
- * Platform conventions:
684
- * - **Linux / macOS**: `$XDG_DATA_HOME/<appName>` or `~/.local/share/<appName>`
685
- * - **Windows**: `%LOCALAPPDATA%/<appName>/Data` or `~/AppData/Local/<appName>/Data`
686
- *
687
- * macOS uses XDG conventions for consistency with Linux.
688
- *
689
- * @param appName - Application name used as directory name. Must be a non-empty
690
- * string without path separators.
691
- * @param env - Optional platform environment override for testing.
692
- * @returns Absolute path to the app's data directory.
693
- * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
694
- *
695
- * @example
696
- * ```ts
697
- * import { dataDir } from "@crustjs/store";
698
- *
699
- * const dir = dataDir("my-cli");
700
- * // → "/home/user/.local/share/my-cli" (Linux)
701
- * // → "/Users/user/.local/share/my-cli" (macOS)
702
- * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Data" (Windows)
703
- * ```
704
- */
705
- declare function dataDir(appName: string, env?: PlatformEnv): string;
584
+ * Resolves the platform-standard data directory for the given app.
585
+ *
586
+ * Platform conventions:
587
+ * - **Linux / macOS**: `$XDG_DATA_HOME/<appName>` or `~/.local/share/<appName>`
588
+ * - **Windows**: `%LOCALAPPDATA%/<appName>/Data` or `~/AppData/Local/<appName>/Data`
589
+ *
590
+ * macOS uses XDG conventions for consistency with Linux.
591
+ *
592
+ * @param appName - Application name used as directory name. Must be a non-empty
593
+ * string without path separators.
594
+ * @param env - Optional platform environment override for testing.
595
+ * @returns Absolute path to the app's data directory.
596
+ * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
597
+ *
598
+ * @example
599
+ * ```ts
600
+ * import { dataDir } from "@crustjs/store";
601
+ *
602
+ * const dir = dataDir("my-cli");
603
+ * // → "/home/user/.local/share/my-cli" (Linux)
604
+ * // → "/Users/user/.local/share/my-cli" (macOS)
605
+ * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Data" (Windows)
606
+ * ```
607
+ */
608
+ export declare function dataDir(appName: string, env?: PlatformEnv): string;
706
609
  /**
707
- * Resolves the platform-standard state directory for the given app.
708
- *
709
- * Platform conventions:
710
- * - **Linux / macOS**: `$XDG_STATE_HOME/<appName>` or `~/.local/state/<appName>`
711
- * - **Windows**: `%LOCALAPPDATA%/<appName>/State` or `~/AppData/Local/<appName>/State`
712
- *
713
- * macOS uses XDG conventions for consistency with Linux.
714
- *
715
- * @param appName - Application name used as directory name. Must be a non-empty
716
- * string without path separators.
717
- * @param env - Optional platform environment override for testing.
718
- * @returns Absolute path to the app's state directory.
719
- * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
720
- *
721
- * @example
722
- * ```ts
723
- * import { stateDir } from "@crustjs/store";
724
- *
725
- * const dir = stateDir("my-cli");
726
- * // → "/home/user/.local/state/my-cli" (Linux)
727
- * // → "/Users/user/.local/state/my-cli" (macOS)
728
- * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\State" (Windows)
729
- * ```
730
- */
731
- declare function stateDir(appName: string, env?: PlatformEnv): string;
610
+ * Resolves the platform-standard state directory for the given app.
611
+ *
612
+ * Platform conventions:
613
+ * - **Linux / macOS**: `$XDG_STATE_HOME/<appName>` or `~/.local/state/<appName>`
614
+ * - **Windows**: `%LOCALAPPDATA%/<appName>/State` or `~/AppData/Local/<appName>/State`
615
+ *
616
+ * macOS uses XDG conventions for consistency with Linux.
617
+ *
618
+ * @param appName - Application name used as directory name. Must be a non-empty
619
+ * string without path separators.
620
+ * @param env - Optional platform environment override for testing.
621
+ * @returns Absolute path to the app's state directory.
622
+ * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
623
+ *
624
+ * @example
625
+ * ```ts
626
+ * import { stateDir } from "@crustjs/store";
627
+ *
628
+ * const dir = stateDir("my-cli");
629
+ * // → "/home/user/.local/state/my-cli" (Linux)
630
+ * // → "/Users/user/.local/state/my-cli" (macOS)
631
+ * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\State" (Windows)
632
+ * ```
633
+ */
634
+ export declare function stateDir(appName: string, env?: PlatformEnv): string;
732
635
  /**
733
- * Resolves the platform-standard cache directory for the given app.
734
- *
735
- * Platform conventions:
736
- * - **Linux / macOS**: `$XDG_CACHE_HOME/<appName>` or `~/.cache/<appName>`
737
- * - **Windows**: `%LOCALAPPDATA%/<appName>/Cache` or `~/AppData/Local/<appName>/Cache`
738
- *
739
- * macOS uses XDG conventions for consistency with Linux.
740
- *
741
- * @param appName - Application name used as directory name. Must be a non-empty
742
- * string without path separators.
743
- * @param env - Optional platform environment override for testing.
744
- * @returns Absolute path to the app's cache directory.
745
- * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
746
- *
747
- * @example
748
- * ```ts
749
- * import { cacheDir } from "@crustjs/store";
750
- *
751
- * const dir = cacheDir("my-cli");
752
- * // → "/home/user/.cache/my-cli" (Linux)
753
- * // → "/Users/user/.cache/my-cli" (macOS)
754
- * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Cache" (Windows)
755
- * ```
756
- */
757
- declare function cacheDir(appName: string, env?: PlatformEnv): string;
636
+ * Resolves the platform-standard cache directory for the given app.
637
+ *
638
+ * Platform conventions:
639
+ * - **Linux / macOS**: `$XDG_CACHE_HOME/<appName>` or `~/.cache/<appName>`
640
+ * - **Windows**: `%LOCALAPPDATA%/<appName>/Cache` or `~/AppData/Local/<appName>/Cache`
641
+ *
642
+ * macOS uses XDG conventions for consistency with Linux.
643
+ *
644
+ * @param appName - Application name used as directory name. Must be a non-empty
645
+ * string without path separators.
646
+ * @param env - Optional platform environment override for testing.
647
+ * @returns Absolute path to the app's cache directory.
648
+ * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
649
+ *
650
+ * @example
651
+ * ```ts
652
+ * import { cacheDir } from "@crustjs/store";
653
+ *
654
+ * const dir = cacheDir("my-cli");
655
+ * // → "/home/user/.cache/my-cli" (Linux)
656
+ * // → "/Users/user/.cache/my-cli" (macOS)
657
+ * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Cache" (Windows)
658
+ * ```
659
+ */
660
+ export declare function cacheDir(appName: string, env?: PlatformEnv): string;
661
+ //#endregion
662
+ //#region src/store.d.ts
758
663
  /**
759
- * Creates a typed async config store backed by a local JSON file.
760
- *
761
- * The store resolves its file path once at creation time from `dirPath` and
762
- * optional `name`. Field definitions declare the config schema — each field's
763
- * `type` determines its TypeScript type, and the presence of `default`
764
- * determines whether the field is guaranteed or optional (`T | undefined`).
765
- *
766
- * @typeParam F - Field definitions record (inferred via `const` generic).
767
- * @param options - Store configuration options.
768
- * @returns A {@link Store} instance with `read`, `write`, `update`, `patch`, and `reset` methods.
769
- * @throws {CrustStoreError} `PATH` if `dirPath` or `name` is invalid.
770
- *
771
- * @example
772
- * ```ts
773
- * import { createStore, configDir } from "@crustjs/store";
774
- *
775
- * const store = createStore({
776
- * dirPath: configDir("my-cli"),
777
- * fields: {
778
- * theme: { type: "string", default: "light" },
779
- * verbose: { type: "boolean", default: false },
780
- * token: { type: "string" },
781
- * },
782
- * });
783
- *
784
- * const config = await store.read();
785
- * // → { theme: "light", verbose: false, token: undefined }
786
- *
787
- * await store.write({ theme: "dark", verbose: true, token: "abc" });
788
- * await store.update((c) => ({ ...c, theme: "light" }));
789
- * await store.patch({ theme: "solarized" });
790
- * await store.reset();
791
- * ```
792
- */
793
- declare function createStore<const F extends FieldsDef>(options: CreateStoreOptions<F>): Store<InferStoreConfig<F>>;
794
- export { stateDir, field, dataDir, createStore, configDir, cacheDir, ValueType, ValidationErrorDetails, StoreValidatorIssue, StoreUpdater, StorePermissionBits, StoreErrorCode, StoreAccess, Store, PlatformEnv, InferStoreConfig, FieldsDef, FieldOptions, FieldDef, DefinitionErrorDetails, CrustStoreError, CreateStoreOptions };
664
+ * Creates a typed async config store backed by a local JSON file.
665
+ *
666
+ * The store resolves its file path once at creation time from `dirPath` and
667
+ * `name`. Core field definitions infer from `type` and `default`;
668
+ * schema-backed fields use the Standard Schema's exact output type.
669
+ *
670
+ * @typeParam F - Field definitions record (inferred via `const` generic).
671
+ * @param options - Store configuration options.
672
+ * @returns A {@link Store} instance with `read`, `write`, `update`, `patch`, and `reset` methods.
673
+ * @throws {CrustStoreError} `PATH` if `dirPath` or `name` is invalid.
674
+ * @throws {CrustStoreError} `DEFINITION` if a field mixes `schema` with `default` or `validate`.
675
+ *
676
+ * @example
677
+ * ```ts
678
+ * import { createStore, configDir } from "@crustjs/store";
679
+ *
680
+ * const store = createStore({
681
+ * dirPath: configDir("my-cli"),
682
+ * name: "config",
683
+ * fields: {
684
+ * theme: { type: "string", default: "light" },
685
+ * verbose: { type: "boolean", default: false },
686
+ * token: { type: "string" },
687
+ * },
688
+ * });
689
+ *
690
+ * const config = await store.read();
691
+ * // → { theme: "light", verbose: false, token: undefined }
692
+ *
693
+ * await store.write({ theme: "dark", verbose: true, token: "abc" });
694
+ * await store.update((c) => ({ ...c, theme: "light" }));
695
+ * await store.patch({ theme: "solarized" });
696
+ * await store.reset();
697
+ * ```
698
+ */
699
+ export declare function createStore<const F extends FieldsDef>(options: CreateStoreOptions<F>): Store<InferStoreConfig<F>>;
700
+ //#endregion
701
+ export type { CreateStoreOptions, FieldDef, FieldsDef, InferStoreConfig, PlatformEnv, Store, StoreAccess, StoreErrorCode, StorePermissionBits, StoreUpdater, StoreValidatorIssue, ValidationErrorDetails, ValueType };