@crustjs/store 0.1.1 → 0.3.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.
package/dist/index.d.ts CHANGED
@@ -1,759 +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;
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;
111
170
  }
112
- interface RawArrayFieldDef extends ArrayFieldBase<unknown[]> {
113
- type?: never;
114
- default?: readonly unknown[];
115
- }
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;
129
- } ? F extends {
130
- default: infer D;
131
- } ? D : O | undefined : F extends {
132
- type: ValueType;
201
+ schema: infer S extends StandardSchema;
202
+ } ? InferOutput<S> : F extends {
203
+ type: ValueType;
133
204
  } ? F extends {
134
- array: true;
205
+ array: true;
135
206
  } ? 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
  }
233
+ /** Explicit Unix permission bits for store persistence. */
234
+ interface StorePermissionBits {
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;
239
+ }
240
+ /**
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
+ */
248
+ type StoreAccess = "default" | "private" | StorePermissionBits;
164
249
  /**
165
- * Options for {@link createStore}.
166
- *
167
- * @typeParam F - The field definitions record (inferred via `const` generic).
168
- *
169
- * @example
170
- * ```ts
171
- * const store = createStore({
172
- * dirPath: configDir("my-cli"),
173
- * fields: {
174
- * theme: { type: "string", default: "light" },
175
- * verbose: { type: "boolean", default: false },
176
- * token: { type: "string" },
177
- * },
178
- * });
179
- * ```
180
- */
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
+ */
181
267
  interface CreateStoreOptions<F extends FieldsDef> {
182
- /**
183
- * Absolute directory path where the store JSON file is persisted.
184
- *
185
- * Use a path helper ({@link configDir}, {@link dataDir}, etc.) to resolve
186
- * the platform-standard directory from an app name.
187
- */
188
- dirPath: string;
189
- /**
190
- * Store name used as the JSON filename in the directory.
191
- *
192
- * Defaults to `"config"`, producing `config.json`. Set to a different value
193
- * to create multiple stores under the same directory
194
- * (e.g. `name: "auth"` → `auth.json`).
195
- *
196
- * Must not contain path separators or the `.json` extension.
197
- */
198
- name?: string;
199
- /**
200
- * Field definitions that declare the store's config schema.
201
- *
202
- * Each key maps to a {@link FieldDef} with a `type` discriminant and optional
203
- * `default` and `validate`. Fields without a `default` are optional (`T | undefined`).
204
- */
205
- fields: F;
206
- /**
207
- * When `true` (the default), persisted keys not present in `fields`
208
- * are dropped on read. Set to `false` to preserve unknown keys.
209
- *
210
- * @default true
211
- */
212
- pruneUnknown?: boolean;
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;
213
316
  }
214
317
  /**
215
- * Receives the current effective config and returns an updated config.
216
- *
217
- * Used by {@link Store.update} to apply mutations atomically.
218
- *
219
- * @example
220
- * ```ts
221
- * await store.update((current) => ({
222
- * ...current,
223
- * theme: "dark",
224
- * }));
225
- * ```
226
- */
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
+ */
227
332
  type StoreUpdater<TConfig> = (current: TConfig) => NoInfer<TConfig>;
228
333
  /**
229
- * A typed async config store returned by {@link createStore}.
230
- *
231
- * Provides `read`, `write`, `update`, `patch`, and `reset` operations for a
232
- * single typed config object persisted as JSON on the local filesystem.
233
- *
234
- * @typeParam TConfig - The inferred config shape from field definitions.
235
- *
236
- * @example
237
- * ```ts
238
- * const store = createStore({
239
- * dirPath: configDir("my-cli"),
240
- * fields: {
241
- * theme: { type: "string", default: "light" },
242
- * verbose: { type: "boolean", default: false },
243
- * },
244
- * });
245
- *
246
- * const config = await store.read();
247
- * await store.write({ theme: "dark", verbose: true });
248
- * await store.update((c) => ({ ...c, theme: "dark" }));
249
- * await store.patch({ theme: "dark" });
250
- * await store.reset();
251
- * ```
252
- */
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
+ */
253
361
  interface Store<TConfig> {
254
- /**
255
- * Reads the persisted config, applying field defaults for missing keys.
256
- *
257
- * Always returns a value — fields with defaults are guaranteed present,
258
- * fields without defaults may be `undefined`.
259
- *
260
- * When fields have `validate` functions, the merged config is validated
261
- * after defaults are applied. Invalid persisted config fails loudly.
262
- *
263
- * @returns The effective config value.
264
- * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
265
- * @throws {CrustStoreError} `VALIDATION` if field validation fails.
266
- * @throws {CrustStoreError} `IO` on filesystem read failures.
267
- */
268
- read(): Promise<TConfig>;
269
- /**
270
- * Atomically persists the full config object.
271
- *
272
- * When fields have `validate` functions, the config is validated before
273
- * persistence.
274
- *
275
- * @param config - The complete config to persist.
276
- * @throws {CrustStoreError} `VALIDATION` if field validation fails.
277
- * @throws {CrustStoreError} `IO` on filesystem write failures.
278
- */
279
- write(config: NoInfer<TConfig>): Promise<void>;
280
- /**
281
- * Reads current effective config, applies the updater, and persists.
282
- *
283
- * @param updater - Function receiving current config and returning updated config.
284
- * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
285
- * @throws {CrustStoreError} `VALIDATION` if field validation fails.
286
- * @throws {CrustStoreError} `IO` on filesystem failures.
287
- */
288
- update(updater: StoreUpdater<TConfig>): Promise<void>;
289
- /**
290
- * Applies a partial update to the current config and persists.
291
- *
292
- * Only the provided keys are updated; everything else is preserved.
293
- *
294
- * @param partial - A partial subset of the config to merge in.
295
- * @throws {CrustStoreError} `VALIDATION` if field validation fails.
296
- * @throws {CrustStoreError} `PARSE` if persisted JSON is malformed.
297
- * @throws {CrustStoreError} `IO` on filesystem failures.
298
- */
299
- patch(partial: Partial<NoInfer<TConfig>>): Promise<void>;
300
- /**
301
- * Removes the persisted config file, returning the store to
302
- * defaults-on-read behavior.
303
- *
304
- * @throws {CrustStoreError} `IO` on filesystem deletion failures.
305
- */
306
- reset(): Promise<void>;
307
- }
308
- /**
309
- * Contextual details attached to a `PATH` error.
310
- *
311
- * Returned when the resolved or provided config file path is invalid.
312
- */
313
- interface PathErrorDetails {
314
- /** The path value that caused the error. */
315
- 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>;
316
426
  }
427
+ //#endregion
428
+ //#region src/errors.d.ts
317
429
  /**
318
- * Contextual details attached to a `PARSE` error.
319
- *
320
- * Returned when the persisted JSON file cannot be parsed.
321
- */
322
- interface ParseErrorDetails {
323
- /** Absolute path to the file that failed parsing. */
324
- path: string;
325
- }
326
- /**
327
- * Contextual details attached to an `IO` error.
328
- *
329
- * Returned on filesystem read, write, or delete failures.
330
- */
430
+ * Contextual details attached to an `IO` error.
431
+ *
432
+ * Returned on filesystem read, write, or delete failures.
433
+ */
331
434
  interface IOErrorDetails {
332
- /** Absolute path to the file involved in the failed operation. */
333
- path: string;
334
- /** The filesystem operation that failed (e.g. `"read"`, `"write"`, `"delete"`). */
335
- 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";
336
439
  }
337
440
  /**
338
- * Contextual details attached to a `VALIDATION` error.
339
- *
340
- * Returned when a store validator rejects a config object during
341
- * read, write, update, or patch operations.
342
- */
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
+ */
343
446
  interface ValidationErrorDetails {
344
- /** The store operation during which validation failed. */
345
- operation: "read" | "write" | "update" | "patch";
346
- /** Structured validation issues for programmatic handling. */
347
- issues: readonly StoreValidatorIssue[];
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[];
348
451
  }
349
452
  /**
350
- * Contextual details attached to a `DEFINITION` error.
351
- *
352
- * Returned when a `field()` call cannot produce a valid `FieldDef` from the
353
- * provided schema and options — e.g. the input is not a Standard Schema v1
354
- * object.
355
- */
356
- interface DefinitionErrorDetails {
357
- /** The schema vendor name, when available. */
358
- vendor?: string;
359
- }
360
- /**
361
- * Maps each {@link StoreErrorCode} to its structured details type.
362
- *
363
- * Used internally for conditional constructor parameters and type narrowing.
364
- */
453
+ * Maps each {@link StoreErrorCode} to its structured details type.
454
+ *
455
+ * Used internally for conditional constructor parameters and type narrowing.
456
+ */
365
457
  interface StoreErrorDetailsMap {
366
- PATH: PathErrorDetails;
367
- PARSE: ParseErrorDetails;
368
- IO: IOErrorDetails;
369
- VALIDATION: ValidationErrorDetails;
370
- DEFINITION: DefinitionErrorDetails;
458
+ PATH: {
459
+ path: string;
460
+ };
461
+ PARSE: {
462
+ path: string;
463
+ };
464
+ IO: IOErrorDetails;
465
+ VALIDATION: ValidationErrorDetails;
466
+ DEFINITION: undefined;
371
467
  }
372
468
  /**
373
- * All possible error codes emitted by `@crustjs/store`.
374
- *
375
- * - `PATH` — Invalid or unsupported config file path
376
- * - `PARSE` — Malformed JSON in persisted config file
377
- * - `IO` — Filesystem read, write, or delete failure
378
- * - `VALIDATION` — Config object rejected by a store validator
379
- * - `DEFINITION` — `field()` cannot produce a valid `FieldDef` from the given schema/options
380
- *
381
- * @example
382
- * ```ts
383
- * import { CrustStoreError } from "@crustjs/store";
384
- *
385
- * try {
386
- * const config = await store.read();
387
- * } catch (err) {
388
- * if (err instanceof CrustStoreError) {
389
- * switch (err.code) {
390
- * case "PARSE":
391
- * console.error(`Corrupt config at ${err.details.path}`);
392
- * break;
393
- * case "VALIDATION":
394
- * for (const issue of err.details.issues) {
395
- * console.error(`${issue.path}: ${issue.message}`);
396
- * }
397
- * break;
398
- * case "IO":
399
- * console.error(`File ${err.details.operation} failed: ${err.message}`);
400
- * break;
401
- * }
402
- * }
403
- * }
404
- * ```
405
- */
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
+ */
406
496
  type StoreErrorCode = keyof StoreErrorDetailsMap;
407
497
  /** Resolves the details type for a given {@link StoreErrorCode}. */
408
498
  type StoreErrorDetails<C extends StoreErrorCode> = StoreErrorDetailsMap[C];
409
499
  /**
410
- * A typed error thrown by `@crustjs/store` for path, parse, IO, and validation failures.
411
- *
412
- * Every `CrustStoreError` carries a {@link StoreErrorCode} that identifies the failure
413
- * category, along with structured {@link StoreErrorDetails} for programmatic handling
414
- * without fragile message parsing.
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 && err.is("PARSE")) {
424
- * console.error(`Corrupt config at ${err.details.path}`);
425
- * }
426
- * }
427
- * ```
428
- */
429
- declare class CrustStoreError<C extends StoreErrorCode = StoreErrorCode> extends Error {
430
- /** Machine-readable error code for programmatic handling. */
431
- readonly code: C;
432
- /** Structured payload with context about the failure. */
433
- readonly details: StoreErrorDetails<C>;
434
- /** Optional wrapped original error or value. */
435
- cause?: unknown;
436
- constructor(code: C, message: string, details: StoreErrorDetails<C>);
437
- /**
438
- * Type-guard that narrows this error to a specific {@link StoreErrorCode}.
439
- *
440
- * When the guard returns `true`, TypeScript narrows the `details` property
441
- * to the corresponding details type.
442
- *
443
- * @example
444
- * ```ts
445
- * if (err.is("IO")) {
446
- * // err.details is IOErrorDetails
447
- * console.error(err.details.operation, err.details.path);
448
- * }
449
- * ```
450
- */
451
- is<T extends StoreErrorCode>(code: T): this is CrustStoreError<T>;
452
- /**
453
- * Attaches a cause to this error for stack-trace chaining.
454
- *
455
- * Returns `this` for fluent usage.
456
- *
457
- * @example
458
- * ```ts
459
- * throw new CrustStoreError("IO", "write failed", {
460
- * path: "/path/to/config.json",
461
- * operation: "write",
462
- * }).withCause(originalError);
463
- * ```
464
- */
465
- withCause(cause: unknown): this;
466
- }
467
- import { BaseValueType as BaseValueType2 } from "@crustjs/utils";
468
- import { InferOutput, StandardSchema } from "@crustjs/utils/schema";
469
- /**
470
- * Optional Crust metadata for the `FieldDef` shape produced by {@link field}.
471
- *
472
- * Every key is optional. Crust does not infer metadata from schemas; schemas
473
- * validate and transform actual runtime values, including missing values passed
474
- * as `undefined`.
475
- *
476
- * No `validate` key — validation flows exclusively through the schema.
477
- * If extra checks are needed, refine the schema with `.refine(...)`
478
- * (Zod), `Schema.filter(...)` (Effect), etc.
479
- *
480
- * @typeParam T - The schema's output value type. Used to type-tighten the
481
- * `default` key when the user wants a non-`undefined` field.
482
- */
483
- interface FieldOptions<T = unknown> {
484
- type?: BaseValueType2;
485
- description?: string;
486
- /**
487
- * Crust metadata default for this field when the persisted state does not
488
- * contain a value for it. Schema defaults should usually live in the schema;
489
- * missing persisted values are validated as `undefined`, so `.default()` works
490
- * naturally at read time.
491
- */
492
- default?: T;
493
- /** Mark this field as an array (collects values into an array). */
494
- 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>;
495
540
  }
496
- type RawSchemaFieldDef<Out> = {
497
- readonly type?: never;
498
- readonly array?: never;
499
- readonly description?: string;
500
- readonly validate: (value: unknown) => Promise<{
501
- value: unknown;
502
- }>;
503
- readonly validateMissing: true;
504
- readonly [FIELD_SCHEMA_OUTPUT]?: Out;
505
- };
506
- type SchemaFieldDefWithOptions<
507
- Out,
508
- Type extends FieldOptions["type"],
509
- Array extends true | undefined,
510
- D = never
511
- > = Omit<RawSchemaFieldDef<Out>, "type" | "array"> & (Type extends NonNullable<FieldOptions["type"]> ? {
512
- readonly type: Type;
513
- } : {
514
- readonly type?: never;
515
- }) & (Array extends true ? {
516
- readonly array: true;
517
- } : {
518
- readonly array?: never;
519
- }) & ([D] extends [never] ? unknown : {
520
- readonly default: D;
521
- });
522
- /**
523
- * `FieldDef` inferred from a Standard Schema, with no narrowed default.
524
- *
525
- * Standard Schema v1 has no spec-portable type-level access to defaults, so
526
- * schema `.default()` does not narrow the inferred TypeScript type. Pass
527
- * `field(schema, { default: x })` explicitly to narrow.
528
- */
529
- type SchemaFieldDef<S extends StandardSchema> = RawSchemaFieldDef<InferOutput<S>>;
530
- /**
531
- * Define a `@crustjs/store` field from any Standard Schema v1.
532
- *
533
- * Returns a value that satisfies store's `FieldDef` discriminated union.
534
- * Crust stores raw values and lets the schema validate and transform them on
535
- * read/write; missing persisted values flow through the schema as `undefined`,
536
- * so `.optional()` and `.default()` behave naturally.
537
- *
538
- * The returned `validate` is an async function that throws an `Error` with
539
- * the schema's normalized issue messages on failure (matches store's
540
- * `FieldDef.validate` contract).
541
- *
542
- * **Type-level defaults**: Standard Schema v1 has no spec-portable type-level
543
- * access to defaults. For tight typing of default-bearing fields, pass
544
- * `default` via `opts`:
545
- *
546
- * ```ts
547
- * field(z.string().default("x")) // value materializes on read; state: string | undefined
548
- * field(z.string(), { default: "x" }) // Crust-level default; state: string
549
- * ```
550
- *
551
- * @param schema - Any Standard Schema v1 object (Zod schemas natively;
552
- * Effect schemas wrapped via `Schema.standardSchemaV1`;
553
- * Valibot/ArkType/Sury/etc. as-is)
554
- * @param opts - Optional Crust-level metadata
555
- * @throws {CrustStoreError} With code `"DEFINITION"` when the input is not
556
- * a Standard Schema v1 object.
557
- *
558
- * @example Zod
559
- * ```ts
560
- * import { z } from "zod";
561
- * import { createStore, configDir, field } from "@crustjs/store";
562
- *
563
- * const store = createStore({
564
- * dirPath: configDir("my-cli"),
565
- * fields: {
566
- * theme: field(z.enum(["light", "dark"]).default("light")),
567
- * verbose: field(z.boolean().default(false)),
568
- * tags: field(z.array(z.string()).default([])),
569
- * },
570
- * });
571
- * ```
572
- *
573
- * @example Effect
574
- * ```ts
575
- * import * as Schema from "effect/Schema";
576
- * import { createStore, configDir, field } from "@crustjs/store";
577
- *
578
- * const store = createStore({
579
- * dirPath: configDir("my-cli"),
580
- * fields: {
581
- * theme: field(Schema.standardSchemaV1(Schema.Literal("light", "dark"))),
582
- * },
583
- * });
584
- * ```
585
- */
586
- declare function field<S extends StandardSchema>(schema: S): SchemaFieldDef<S>;
587
- declare function field<
588
- S extends StandardSchema,
589
- D extends InferOutput<S>,
590
- const Type extends FieldOptions<InferOutput<S>>["type"] = undefined,
591
- const Array extends true | undefined = undefined
592
- >(schema: S, opts: FieldOptions<InferOutput<S>> & {
593
- default: D;
594
- type?: Type;
595
- array?: Array;
596
- }): SchemaFieldDefWithOptions<InferOutput<S>, Type, Array, D>;
597
- declare function field<
598
- S extends StandardSchema,
599
- const Type extends FieldOptions<InferOutput<S>>["type"] = undefined,
600
- const Array extends true | undefined = undefined
601
- >(schema: S, opts: FieldOptions<InferOutput<S>> & {
602
- type?: Type;
603
- array?: Array;
604
- }): SchemaFieldDefWithOptions<InferOutput<S>, Type, Array>;
541
+ //#endregion
542
+ //#region src/path.d.ts
605
543
  /**
606
- * Encapsulates runtime environment values needed for path resolution.
607
- *
608
- * Exposed as a parameter to allow deterministic testing without mutating
609
- * `process.env` or `process.platform`.
610
- */
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
+ */
611
549
  interface PlatformEnv {
612
- /** Operating system platform identifier (e.g. `"linux"`, `"darwin"`, `"win32"`). */
613
- platform: string;
614
- /** Environment variables map. */
615
- env: Record<string, string | undefined>;
616
- /** User home directory path. */
617
- 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;
618
556
  }
619
557
  /**
620
- * Resolves the platform-standard config directory for the given app.
621
- *
622
- * Platform conventions:
623
- * - **Linux / macOS**: `$XDG_CONFIG_HOME/<appName>` or `~/.config/<appName>`
624
- * - **Windows**: `%APPDATA%/<appName>` or `~/AppData/Roaming/<appName>`
625
- *
626
- * macOS uses XDG conventions for consistency with Linux.
627
- *
628
- * @param appName - Application name used as directory name. Must be a non-empty
629
- * string without path separators.
630
- * @param env - Optional platform environment override for testing.
631
- * @returns Absolute path to the app's config directory.
632
- * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
633
- *
634
- * @example
635
- * ```ts
636
- * import { configDir } from "@crustjs/store";
637
- *
638
- * const dir = configDir("my-cli");
639
- * // → "/home/user/.config/my-cli" (Linux)
640
- * // → "/Users/user/.config/my-cli" (macOS)
641
- * // → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
642
- * ```
643
- */
644
- 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;
645
583
  /**
646
- * Resolves the platform-standard data directory for the given app.
647
- *
648
- * Platform conventions:
649
- * - **Linux / macOS**: `$XDG_DATA_HOME/<appName>` or `~/.local/share/<appName>`
650
- * - **Windows**: `%LOCALAPPDATA%/<appName>/Data` or `~/AppData/Local/<appName>/Data`
651
- *
652
- * macOS uses XDG conventions for consistency with Linux.
653
- *
654
- * @param appName - Application name used as directory name. Must be a non-empty
655
- * string without path separators.
656
- * @param env - Optional platform environment override for testing.
657
- * @returns Absolute path to the app's data directory.
658
- * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
659
- *
660
- * @example
661
- * ```ts
662
- * import { dataDir } from "@crustjs/store";
663
- *
664
- * const dir = dataDir("my-cli");
665
- * // → "/home/user/.local/share/my-cli" (Linux)
666
- * // → "/Users/user/.local/share/my-cli" (macOS)
667
- * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Data" (Windows)
668
- * ```
669
- */
670
- 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;
671
609
  /**
672
- * Resolves the platform-standard state directory for the given app.
673
- *
674
- * Platform conventions:
675
- * - **Linux / macOS**: `$XDG_STATE_HOME/<appName>` or `~/.local/state/<appName>`
676
- * - **Windows**: `%LOCALAPPDATA%/<appName>/State` or `~/AppData/Local/<appName>/State`
677
- *
678
- * macOS uses XDG conventions for consistency with Linux.
679
- *
680
- * @param appName - Application name used as directory name. Must be a non-empty
681
- * string without path separators.
682
- * @param env - Optional platform environment override for testing.
683
- * @returns Absolute path to the app's state directory.
684
- * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
685
- *
686
- * @example
687
- * ```ts
688
- * import { stateDir } from "@crustjs/store";
689
- *
690
- * const dir = stateDir("my-cli");
691
- * // → "/home/user/.local/state/my-cli" (Linux)
692
- * // → "/Users/user/.local/state/my-cli" (macOS)
693
- * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\State" (Windows)
694
- * ```
695
- */
696
- 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;
697
635
  /**
698
- * Resolves the platform-standard cache directory for the given app.
699
- *
700
- * Platform conventions:
701
- * - **Linux / macOS**: `$XDG_CACHE_HOME/<appName>` or `~/.cache/<appName>`
702
- * - **Windows**: `%LOCALAPPDATA%/<appName>/Cache` or `~/AppData/Local/<appName>/Cache`
703
- *
704
- * macOS uses XDG conventions for consistency with Linux.
705
- *
706
- * @param appName - Application name used as directory name. Must be a non-empty
707
- * string without path separators.
708
- * @param env - Optional platform environment override for testing.
709
- * @returns Absolute path to the app's cache directory.
710
- * @throws {CrustStoreError} `PATH` if `appName` is invalid or platform is unsupported.
711
- *
712
- * @example
713
- * ```ts
714
- * import { cacheDir } from "@crustjs/store";
715
- *
716
- * const dir = cacheDir("my-cli");
717
- * // → "/home/user/.cache/my-cli" (Linux)
718
- * // → "/Users/user/.cache/my-cli" (macOS)
719
- * // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Cache" (Windows)
720
- * ```
721
- */
722
- 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
723
663
  /**
724
- * Creates a typed async config store backed by a local JSON file.
725
- *
726
- * The store resolves its file path once at creation time from `dirPath` and
727
- * optional `name`. Field definitions declare the config schema — each field's
728
- * `type` determines its TypeScript type, and the presence of `default`
729
- * determines whether the field is guaranteed or optional (`T | undefined`).
730
- *
731
- * @typeParam F - Field definitions record (inferred via `const` generic).
732
- * @param options - Store configuration options.
733
- * @returns A {@link Store} instance with `read`, `write`, `update`, `patch`, and `reset` methods.
734
- * @throws {CrustStoreError} `PATH` if `dirPath` or `name` is invalid.
735
- *
736
- * @example
737
- * ```ts
738
- * import { createStore, configDir } from "@crustjs/store";
739
- *
740
- * const store = createStore({
741
- * dirPath: configDir("my-cli"),
742
- * fields: {
743
- * theme: { type: "string", default: "light" },
744
- * verbose: { type: "boolean", default: false },
745
- * token: { type: "string" },
746
- * },
747
- * });
748
- *
749
- * const config = await store.read();
750
- * // → { theme: "light", verbose: false, token: undefined }
751
- *
752
- * await store.write({ theme: "dark", verbose: true, token: "abc" });
753
- * await store.update((c) => ({ ...c, theme: "light" }));
754
- * await store.patch({ theme: "solarized" });
755
- * await store.reset();
756
- * ```
757
- */
758
- declare function createStore<const F extends FieldsDef>(options: CreateStoreOptions<F>): Store<InferStoreConfig<F>>;
759
- export { stateDir, field, dataDir, createStore, configDir, cacheDir, ValueType, ValidationErrorDetails, StoreValidatorIssue, StoreUpdater, StoreErrorCode, 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 };