@kidd-cli/core 0.1.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/dist/config-BvGapuFJ.js +282 -0
  4. package/dist/config-BvGapuFJ.js.map +1 -0
  5. package/dist/create-store-BQUX0tAn.js +197 -0
  6. package/dist/create-store-BQUX0tAn.js.map +1 -0
  7. package/dist/index.d.ts +73 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +1034 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/lib/config.d.ts +64 -0
  12. package/dist/lib/config.d.ts.map +1 -0
  13. package/dist/lib/config.js +4 -0
  14. package/dist/lib/logger.d.ts +2 -0
  15. package/dist/lib/logger.js +55 -0
  16. package/dist/lib/logger.js.map +1 -0
  17. package/dist/lib/output.d.ts +62 -0
  18. package/dist/lib/output.d.ts.map +1 -0
  19. package/dist/lib/output.js +276 -0
  20. package/dist/lib/output.js.map +1 -0
  21. package/dist/lib/project.d.ts +59 -0
  22. package/dist/lib/project.d.ts.map +1 -0
  23. package/dist/lib/project.js +3 -0
  24. package/dist/lib/prompts.d.ts +24 -0
  25. package/dist/lib/prompts.d.ts.map +1 -0
  26. package/dist/lib/prompts.js +3 -0
  27. package/dist/lib/store.d.ts +56 -0
  28. package/dist/lib/store.d.ts.map +1 -0
  29. package/dist/lib/store.js +4 -0
  30. package/dist/logger-BkQQej8h.d.ts +76 -0
  31. package/dist/logger-BkQQej8h.d.ts.map +1 -0
  32. package/dist/middleware/auth.d.ts +22 -0
  33. package/dist/middleware/auth.d.ts.map +1 -0
  34. package/dist/middleware/auth.js +759 -0
  35. package/dist/middleware/auth.js.map +1 -0
  36. package/dist/middleware/http.d.ts +87 -0
  37. package/dist/middleware/http.d.ts.map +1 -0
  38. package/dist/middleware/http.js +255 -0
  39. package/dist/middleware/http.js.map +1 -0
  40. package/dist/middleware-D3psyhYo.js +54 -0
  41. package/dist/middleware-D3psyhYo.js.map +1 -0
  42. package/dist/project-NPtYX2ZX.js +181 -0
  43. package/dist/project-NPtYX2ZX.js.map +1 -0
  44. package/dist/prompts-lLfUSgd6.js +63 -0
  45. package/dist/prompts-lLfUSgd6.js.map +1 -0
  46. package/dist/types-CqKJhsYk.d.ts +135 -0
  47. package/dist/types-CqKJhsYk.d.ts.map +1 -0
  48. package/dist/types-Cz9h927W.d.ts +23 -0
  49. package/dist/types-Cz9h927W.d.ts.map +1 -0
  50. package/dist/types-DFtYg5uZ.d.ts +26 -0
  51. package/dist/types-DFtYg5uZ.d.ts.map +1 -0
  52. package/dist/types-kjpRau0U.d.ts +382 -0
  53. package/dist/types-kjpRau0U.d.ts.map +1 -0
  54. package/package.json +94 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Joggr, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # kidd
2
+
3
+ An opinionated CLI framework for Node.js built on yargs and Zod.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add kidd
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { cli, command } from 'kidd'
15
+ import { z } from 'zod'
16
+
17
+ const greet = command({
18
+ description: 'Greet a user',
19
+ args: z.object({
20
+ name: z.string().describe('Name to greet'),
21
+ }),
22
+ async handler(ctx) {
23
+ ctx.logger.info(`Hello, ${ctx.args.name}!`)
24
+ },
25
+ })
26
+
27
+ await cli({
28
+ name: 'my-app',
29
+ version: '1.0.0',
30
+ commands: { greet },
31
+ })
32
+ ```
33
+
34
+ ## API
35
+
36
+ ### `cli()`
37
+
38
+ Bootstrap and run the CLI application.
39
+
40
+ ```ts
41
+ cli({
42
+ name: 'my-app',
43
+ version: '1.0.0',
44
+ description: 'My CLI tool',
45
+ commands: { deploy, migrate },
46
+ middleware: [requireAuth()],
47
+ config: { schema: MyConfigSchema },
48
+ credentials: {
49
+ apiKey: { env: 'API_KEY', required: true },
50
+ },
51
+ })
52
+ ```
53
+
54
+ ### `command()`
55
+
56
+ Create a command definition.
57
+
58
+ ```ts
59
+ const deploy = command({
60
+ description: 'Deploy the application',
61
+ args: z.object({
62
+ env: z.enum(['staging', 'production']).describe('Target environment'),
63
+ dryRun: z.boolean().default(false).describe('Preview without applying'),
64
+ }),
65
+ async handler(ctx) {
66
+ ctx.logger.info(`Deploying to ${ctx.args.env}`)
67
+ },
68
+ })
69
+ ```
70
+
71
+ ### `middleware()`
72
+
73
+ Create a middleware that runs before command handlers.
74
+
75
+ ```ts
76
+ const timing = middleware(async (ctx, next) => {
77
+ const start = Date.now()
78
+ await next()
79
+ ctx.logger.info(`Completed in ${Date.now() - start}ms`)
80
+ })
81
+ ```
82
+
83
+ ### `autoload()`
84
+
85
+ Dynamically discover commands from a directory.
86
+
87
+ ```ts
88
+ cli({
89
+ name: 'my-app',
90
+ version: '1.0.0',
91
+ commands: {
92
+ generate: autoload({ dir: './commands/generate' }),
93
+ },
94
+ })
95
+ ```
96
+
97
+ ### `defineConfig()`
98
+
99
+ Type-safe helper for `kidd.config.ts` files.
100
+
101
+ ```ts
102
+ import { defineConfig } from 'kidd'
103
+
104
+ export default defineConfig({
105
+ build: { out: 'dist' },
106
+ })
107
+ ```
108
+
109
+ ## Sub-exports
110
+
111
+ ### `kidd/prompts`
112
+
113
+ Interactive terminal prompts backed by `@clack/prompts`.
114
+
115
+ ```ts
116
+ import { prompts, spinner } from 'kidd/prompts'
117
+
118
+ const name = await prompts.text({ message: 'Project name?' })
119
+
120
+ spinner.start('Building...')
121
+ spinner.stop('Done')
122
+ ```
123
+
124
+ ### `kidd/logger`
125
+
126
+ Structured terminal logger backed by `@clack/prompts`.
127
+
128
+ ```ts
129
+ import { log } from 'kidd/logger'
130
+
131
+ log.intro('My CLI')
132
+ log.info('Processing...')
133
+ log.success('Complete')
134
+ log.outro('Done')
135
+ ```
136
+
137
+ ### `kidd/output`
138
+
139
+ Structured output for JSON, templates, and files.
140
+
141
+ ```ts
142
+ import { output } from 'kidd/output'
143
+
144
+ output.json({ status: 'ok' })
145
+ output.write({ path: './out.json', content: output.toJson(data) })
146
+ ```
147
+
148
+ ### `kidd/errors`
149
+
150
+ Error creation, formatting, and sanitization utilities.
151
+
152
+ ```ts
153
+ import { createErrorUtil, sanitize } from 'kidd/errors'
154
+
155
+ const errors = createErrorUtil({ prefix: 'deploy', sanitize: true })
156
+ const err = errors.create('Connection refused')
157
+ const msg = errors.getMessage(unknownError)
158
+ const clean = sanitize('token=abc123&secret=xyz')
159
+ ```
160
+
161
+ ### `kidd/config`
162
+
163
+ Typed config client for JSON/JSONC/YAML files.
164
+
165
+ ```ts
166
+ import { createConfigClient } from 'kidd/config'
167
+
168
+ const config = createConfigClient({ name: 'my-app', schema: MySchema })
169
+ const [error, result] = await config.load()
170
+ ```
171
+
172
+ ### `kidd/store`
173
+
174
+ File-backed JSON store with local and global resolution.
175
+
176
+ ```ts
177
+ import { createStore } from 'kidd/store'
178
+
179
+ const store = createStore({ dirName: '.my-app' })
180
+ const settings = store.load('settings.json')
181
+ ```
182
+
183
+ ### `kidd/validate`
184
+
185
+ Zod-based validation returning Result tuples.
186
+
187
+ ```ts
188
+ import { validate } from 'kidd/validate'
189
+
190
+ const [error, value] = validate(
191
+ MySchema,
192
+ input,
193
+ (zodError) => new Error(`Invalid: ${zodError.message}`)
194
+ )
195
+ ```
196
+
197
+ ### `kidd/project`
198
+
199
+ Git project root resolution, submodule detection, and dotenv loading.
200
+
201
+ ```ts
202
+ import { findProjectRoot, createDotEnv, isInSubmodule } from 'kidd/project'
203
+
204
+ const root = findProjectRoot()
205
+ const env = createDotEnv({ dirName: '.my-app' })
206
+ const vars = await env.load()
207
+ const submodule = isInSubmodule()
208
+ ```
209
+
210
+ ## References
211
+
212
+ - [yargs](https://yargs.js.org)
213
+ - [Zod](https://zod.dev)
214
+ - [@clack/prompts](https://www.clack.cc)
@@ -0,0 +1,282 @@
1
+ import { i as findProjectRoot } from "./project-NPtYX2ZX.js";
2
+ import { dirname, extname, join } from "node:path";
3
+ import { attempt, attemptAsync, err, match } from "@kidd-cli/utils/fp";
4
+ import { jsonParse, jsonStringify } from "@kidd-cli/utils/json";
5
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
6
+ import { formatZodIssues } from "@kidd-cli/utils/validate";
7
+ import { parse, printParseErrorCode } from "jsonc-parser";
8
+ import { parse as parse$1, stringify } from "yaml";
9
+
10
+ //#region src/utils/constants.ts
11
+ /**
12
+ * Default process exit code for error conditions.
13
+ */
14
+ const DEFAULT_EXIT_CODE = 1;
15
+
16
+ //#endregion
17
+ //#region src/lib/config/constants.ts
18
+ const EMPTY_LENGTH = 0;
19
+ const CONFIG_EXTENSIONS = [
20
+ ".jsonc",
21
+ ".json",
22
+ ".yaml"
23
+ ];
24
+
25
+ //#endregion
26
+ //#region src/lib/config/find.ts
27
+ /**
28
+ * Generate the list of config file names to search for based on the CLI name.
29
+ *
30
+ * Produces names like `.myapp.jsonc`, `.myapp.json`, `.myapp.yaml` from the
31
+ * supported extension list.
32
+ *
33
+ * @param name - The CLI name used to derive config file names.
34
+ * @returns An array of config file names to search for.
35
+ */
36
+ function getConfigFileNames(name) {
37
+ return CONFIG_EXTENSIONS.map((ext) => `.${name}${ext}`);
38
+ }
39
+ /**
40
+ * Check whether a file exists at the given path.
41
+ *
42
+ * @param filePath - The absolute file path to check.
43
+ * @returns True when the file exists and is accessible.
44
+ */
45
+ async function fileExists(filePath) {
46
+ const [error] = await attemptAsync(() => access(filePath));
47
+ return !error;
48
+ }
49
+ /**
50
+ * Search a single directory for the first matching config file name.
51
+ *
52
+ * Checks each candidate file name in order and returns the path of the first
53
+ * one that exists on disk.
54
+ *
55
+ * @param dir - The directory to search in.
56
+ * @param fileNames - Candidate config file names to look for.
57
+ * @returns The full path to the first matching config file, or null if none found.
58
+ */
59
+ async function findConfigFile(dir, fileNames) {
60
+ return (await Promise.all(fileNames.map(async (fileName) => {
61
+ const filePath = join(dir, fileName);
62
+ if (await fileExists(filePath)) return filePath;
63
+ return null;
64
+ }))).find((result) => result !== null) ?? null;
65
+ }
66
+ /**
67
+ * Search for a config file across multiple directories.
68
+ *
69
+ * Searches in order: explicit search paths, the current working directory,
70
+ * and the project root (if different from cwd). Returns the path of the
71
+ * first matching file found.
72
+ *
73
+ * @param options - Search options including cwd, file names, and optional search paths.
74
+ * @returns The full path to the config file, or null if not found.
75
+ */
76
+ async function findConfig(options) {
77
+ const { fileNames, cwd, searchPaths } = options;
78
+ if (searchPaths) {
79
+ const found = (await Promise.all(searchPaths.map((dir) => findConfigFile(dir, fileNames)))).find((result) => result !== null);
80
+ if (found) return found;
81
+ }
82
+ const fromCwd = await findConfigFile(cwd, fileNames);
83
+ if (fromCwd) return fromCwd;
84
+ const projectRoot = findProjectRoot(cwd);
85
+ if (projectRoot && projectRoot.path !== cwd) {
86
+ const fromRoot = await findConfigFile(projectRoot.path, fileNames);
87
+ if (fromRoot) return fromRoot;
88
+ }
89
+ return null;
90
+ }
91
+
92
+ //#endregion
93
+ //#region src/lib/config/parse.ts
94
+ /**
95
+ * Determine the config format from a file path's extension.
96
+ *
97
+ * @param filePath - The file path to inspect.
98
+ * @returns The detected config format ('json', 'jsonc', or 'yaml').
99
+ */
100
+ function getFormat(filePath) {
101
+ return match(extname(filePath)).with(".jsonc", () => "jsonc").with(".yaml", () => "yaml").otherwise(() => "json");
102
+ }
103
+ /**
104
+ * Parse a JSON string and return the result as a ConfigOperationResult.
105
+ *
106
+ * @param content - The raw JSON string to parse.
107
+ * @param filePath - The file path used in error messages.
108
+ * @returns A ConfigOperationResult with the parsed data or a parse error.
109
+ */
110
+ function parseJson(content, filePath) {
111
+ const [error, result] = jsonParse(content);
112
+ if (error) return err(`Failed to parse JSON in ${filePath}: ${error.message}`);
113
+ return [null, result];
114
+ }
115
+ /**
116
+ * Parse a JSONC (JSON with comments) string and return the result as a ConfigOperationResult.
117
+ *
118
+ * @param content - The raw JSONC string to parse.
119
+ * @param filePath - The file path used in error messages.
120
+ * @returns A ConfigOperationResult with the parsed data or a parse error.
121
+ */
122
+ function parseJsoncContent(content, filePath) {
123
+ const errors = [];
124
+ const result = parse(content, errors, {
125
+ allowEmptyContent: false,
126
+ allowTrailingComma: true
127
+ });
128
+ if (errors.length > EMPTY_LENGTH) return err(`Failed to parse JSONC in ${filePath}:\n${errors.map((parseError) => ` - ${printParseErrorCode(parseError.error)} at offset ${parseError.offset}`).join("\n")}`);
129
+ return [null, result];
130
+ }
131
+ /**
132
+ * Parse a YAML string and return the result as a ConfigOperationResult.
133
+ *
134
+ * @param content - The raw YAML string to parse.
135
+ * @param filePath - The file path used in error messages.
136
+ * @returns A ConfigOperationResult with the parsed data or a parse error.
137
+ */
138
+ function parseYamlContent(content, filePath) {
139
+ const [error, result] = attempt(() => parse$1(content));
140
+ if (error) return err(`Failed to parse YAML in ${filePath}: ${String(error)}`);
141
+ return [null, result];
142
+ }
143
+ /**
144
+ * Parse config file content using the appropriate parser for the given format.
145
+ *
146
+ * @param options - Parse content options.
147
+ * @returns A ConfigOperationResult with the parsed data or an error.
148
+ */
149
+ function parseContent(options) {
150
+ const { content, filePath, format } = options;
151
+ return match(format).with("json", () => parseJson(content, filePath)).with("jsonc", () => parseJsoncContent(content, filePath)).with("yaml", () => parseYamlContent(content, filePath)).exhaustive();
152
+ }
153
+ /**
154
+ * Serialize data to a string in the specified config format.
155
+ *
156
+ * @param data - The data to serialize.
157
+ * @param format - The target config format.
158
+ * @returns The serialized string representation.
159
+ */
160
+ function serializeContent(data, format) {
161
+ return match(format).with("json", () => {
162
+ const [, json] = jsonStringify(data, { pretty: true });
163
+ return `${json}\n`;
164
+ }).with("jsonc", () => {
165
+ const [, json] = jsonStringify(data, { pretty: true });
166
+ return `${json}\n`;
167
+ }).with("yaml", () => stringify(data)).exhaustive();
168
+ }
169
+ /**
170
+ * Get the file extension string for a given config format.
171
+ *
172
+ * @param format - The config format.
173
+ * @returns The file extension including the leading dot (e.g. '.json').
174
+ */
175
+ function getExtension(format) {
176
+ return match(format).with("json", () => ".json").with("jsonc", () => ".jsonc").with("yaml", () => ".yaml").exhaustive();
177
+ }
178
+
179
+ //#endregion
180
+ //#region src/lib/config/create-config.ts
181
+ /**
182
+ * Create a typed config client that loads, validates, and writes config files.
183
+ *
184
+ * @param options - Config client options including name and Zod schema.
185
+ * @returns A {@link Config} client instance.
186
+ */
187
+ function createConfigClient(options) {
188
+ const { name, schema, searchPaths } = options;
189
+ const fileNames = getConfigFileNames(name);
190
+ /**
191
+ * Find a config file in the given directory.
192
+ *
193
+ * @private
194
+ * @param cwd - Working directory to search from.
195
+ * @returns The path to the config file, or null if not found.
196
+ */
197
+ async function find(cwd) {
198
+ return findConfig({
199
+ cwd: cwd ?? process.cwd(),
200
+ fileNames,
201
+ searchPaths
202
+ });
203
+ }
204
+ /**
205
+ * Load and validate a config file.
206
+ *
207
+ * @private
208
+ * @param cwd - Working directory to search from.
209
+ * @returns A ConfigOperationResult with the loaded config, or [null, null] if not found.
210
+ */
211
+ async function load(cwd) {
212
+ const filePath = await find(cwd);
213
+ if (!filePath) return [null, null];
214
+ const [readError, content] = await attemptAsync(() => readFile(filePath, "utf8"));
215
+ if (readError || content === null) return err(`Failed to read config at ${filePath}: ${resolveReadErrorDetail(readError)}`);
216
+ const format = getFormat(filePath);
217
+ const parsedResult = parseContent({
218
+ content,
219
+ filePath,
220
+ format
221
+ });
222
+ if (parsedResult[0]) return [parsedResult[0], null];
223
+ const result = schema.safeParse(parsedResult[1]);
224
+ if (!result.success) {
225
+ const { message } = formatZodIssues(result.error.issues, "\n");
226
+ return err(`Invalid config in ${filePath}:\n${message}`);
227
+ }
228
+ return [null, {
229
+ config: result.data,
230
+ filePath,
231
+ format
232
+ }];
233
+ }
234
+ /**
235
+ * Validate and write config data to a file.
236
+ *
237
+ * @private
238
+ * @param data - The config data to write.
239
+ * @param writeOptions - Write options including path and format.
240
+ * @returns A ConfigOperationResult with the write result.
241
+ */
242
+ async function write(data, writeOptions = {}) {
243
+ const result = schema.safeParse(data);
244
+ if (!result.success) {
245
+ const { message } = formatZodIssues(result.error.issues, "\n");
246
+ return err(`Invalid config data:\n${message}`);
247
+ }
248
+ const resolvedFormat = match(writeOptions).when((opts) => opts.format !== null && opts.format !== void 0, (opts) => opts.format ?? "jsonc").when((opts) => opts.filePath !== null && opts.filePath !== void 0, (opts) => getFormat(opts.filePath ?? "")).otherwise(() => "jsonc");
249
+ const resolvedFilePath = match(writeOptions.filePath).when((fp) => fp !== null && fp !== void 0, (fp) => fp ?? "").otherwise(() => {
250
+ return join(writeOptions.dir ?? process.cwd(), `.${name}${getExtension(resolvedFormat)}`);
251
+ });
252
+ const serialized = serializeContent(result.data, resolvedFormat);
253
+ const [mkdirError] = await attemptAsync(() => mkdir(dirname(resolvedFilePath), { recursive: true }));
254
+ if (mkdirError) return err(`Failed to create directory for ${resolvedFilePath}: ${String(mkdirError)}`);
255
+ const [writeError] = await attemptAsync(() => writeFile(resolvedFilePath, serialized, "utf8"));
256
+ if (writeError) return err(`Failed to write config to ${resolvedFilePath}: ${String(writeError)}`);
257
+ return [null, {
258
+ filePath: resolvedFilePath,
259
+ format: resolvedFormat
260
+ }];
261
+ }
262
+ return {
263
+ find,
264
+ load,
265
+ write
266
+ };
267
+ }
268
+ /**
269
+ * Resolve the error detail string from a read error.
270
+ *
271
+ * @private
272
+ * @param readError - The error from the read operation, or null.
273
+ * @returns A descriptive error string.
274
+ */
275
+ function resolveReadErrorDetail(readError) {
276
+ if (readError) return String(readError);
277
+ return "empty file";
278
+ }
279
+
280
+ //#endregion
281
+ export { DEFAULT_EXIT_CODE as n, createConfigClient as t };
282
+ //# sourceMappingURL=config-BvGapuFJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-BvGapuFJ.js","names":["parseJsonc","parseYaml","stringifyYaml"],"sources":["../src/utils/constants.ts","../src/lib/config/constants.ts","../src/lib/config/find.ts","../src/lib/config/parse.ts","../src/lib/config/create-config.ts"],"sourcesContent":["/**\n * Standard JSON indentation level used across the package.\n */\nexport const JSON_INDENT = 2\n\n/**\n * Default process exit code for error conditions.\n */\nexport const DEFAULT_EXIT_CODE = 1\n","/**\n * Supported configuration file formats.\n */\nexport type ConfigFormat = 'json' | 'jsonc' | 'yaml'\n\nexport { JSON_INDENT } from '@/utils/constants.js'\nexport const EMPTY_LENGTH = 0\nexport const CONFIG_EXTENSIONS = ['.jsonc', '.json', '.yaml'] as const\n","import { access } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nimport { attemptAsync } from '@kidd-cli/utils/fp'\n\nimport { findProjectRoot } from '@/lib/project/index.js'\n\nimport { CONFIG_EXTENSIONS } from './constants.js'\n\n/**\n * Generate the list of config file names to search for based on the CLI name.\n *\n * Produces names like `.myapp.jsonc`, `.myapp.json`, `.myapp.yaml` from the\n * supported extension list.\n *\n * @param name - The CLI name used to derive config file names.\n * @returns An array of config file names to search for.\n */\nexport function getConfigFileNames(name: string): string[] {\n return CONFIG_EXTENSIONS.map((ext) => `.${name}${ext}`)\n}\n\n/**\n * Check whether a file exists at the given path.\n *\n * @param filePath - The absolute file path to check.\n * @returns True when the file exists and is accessible.\n */\nexport async function fileExists(filePath: string): Promise<boolean> {\n const [error] = await attemptAsync(() => access(filePath))\n return !error\n}\n\n/**\n * Search a single directory for the first matching config file name.\n *\n * Checks each candidate file name in order and returns the path of the first\n * one that exists on disk.\n *\n * @param dir - The directory to search in.\n * @param fileNames - Candidate config file names to look for.\n * @returns The full path to the first matching config file, or null if none found.\n */\nexport async function findConfigFile(\n dir: string,\n fileNames: readonly string[]\n): Promise<string | null> {\n const results = await Promise.all(\n fileNames.map(async (fileName) => {\n const filePath = join(dir, fileName)\n const exists = await fileExists(filePath)\n if (exists) {\n return filePath\n }\n return null\n })\n )\n const found = results.find((result): result is string => result !== null)\n return found ?? null\n}\n\n/**\n * Search for a config file across multiple directories.\n *\n * Searches in order: explicit search paths, the current working directory,\n * and the project root (if different from cwd). Returns the path of the\n * first matching file found.\n *\n * @param options - Search options including cwd, file names, and optional search paths.\n * @returns The full path to the config file, or null if not found.\n */\nexport async function findConfig(options: {\n cwd: string\n fileNames: string[]\n searchPaths?: string[]\n}): Promise<string | null> {\n const { fileNames, cwd, searchPaths } = options\n\n if (searchPaths) {\n const searchResults = await Promise.all(\n searchPaths.map((dir) => findConfigFile(dir, fileNames))\n )\n const found = searchResults.find((result): result is string => result !== null)\n if (found) {\n return found\n }\n }\n\n const fromCwd = await findConfigFile(cwd, fileNames)\n if (fromCwd) {\n return fromCwd\n }\n\n const projectRoot = findProjectRoot(cwd)\n if (projectRoot && projectRoot.path !== cwd) {\n const fromRoot = await findConfigFile(projectRoot.path, fileNames)\n if (fromRoot) {\n return fromRoot\n }\n }\n\n return null\n}\n","import { extname } from 'node:path'\n\nimport { attempt, err, match } from '@kidd-cli/utils/fp'\nimport { jsonParse, jsonStringify } from '@kidd-cli/utils/json'\nimport type { ParseError } from 'jsonc-parser'\nimport { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser'\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml'\n\nimport type { ConfigFormat } from './constants.js'\nimport { EMPTY_LENGTH } from './constants.js'\nimport type { ConfigOperationResult } from './types.js'\n\n/**\n * Determine the config format from a file path's extension.\n *\n * @param filePath - The file path to inspect.\n * @returns The detected config format ('json', 'jsonc', or 'yaml').\n */\nexport function getFormat(filePath: string): ConfigFormat {\n const ext = extname(filePath)\n return match(ext)\n .with('.jsonc', () => 'jsonc' as const)\n .with('.yaml', () => 'yaml' as const)\n .otherwise(() => 'json' as const)\n}\n\n/**\n * Parse a JSON string and return the result as a ConfigOperationResult.\n *\n * @param content - The raw JSON string to parse.\n * @param filePath - The file path used in error messages.\n * @returns A ConfigOperationResult with the parsed data or a parse error.\n */\nexport function parseJson(content: string, filePath: string): ConfigOperationResult<unknown> {\n const [error, result] = jsonParse(content)\n if (error) {\n return err(`Failed to parse JSON in ${filePath}: ${error.message}`)\n }\n return [null, result]\n}\n\n/**\n * Parse a JSONC (JSON with comments) string and return the result as a ConfigOperationResult.\n *\n * @param content - The raw JSONC string to parse.\n * @param filePath - The file path used in error messages.\n * @returns A ConfigOperationResult with the parsed data or a parse error.\n */\nexport function parseJsoncContent(\n content: string,\n filePath: string\n): ConfigOperationResult<unknown> {\n const errors: ParseError[] = []\n const result = parseJsonc(content, errors, {\n allowEmptyContent: false,\n allowTrailingComma: true,\n })\n if (errors.length > EMPTY_LENGTH) {\n const errorMessages = errors\n .map(\n (parseError) =>\n ` - ${printParseErrorCode(parseError.error)} at offset ${parseError.offset}`\n )\n .join('\\n')\n return err(`Failed to parse JSONC in ${filePath}:\\n${errorMessages}`)\n }\n return [null, result]\n}\n\n/**\n * Parse a YAML string and return the result as a ConfigOperationResult.\n *\n * @param content - The raw YAML string to parse.\n * @param filePath - The file path used in error messages.\n * @returns A ConfigOperationResult with the parsed data or a parse error.\n */\nexport function parseYamlContent(\n content: string,\n filePath: string\n): ConfigOperationResult<unknown> {\n const [error, result] = attempt(() => parseYaml(content))\n if (error) {\n return err(`Failed to parse YAML in ${filePath}: ${String(error)}`)\n }\n return [null, result]\n}\n\n/**\n * Options for parsing config file content.\n */\nexport interface ParseContentOptions {\n readonly content: string\n readonly filePath: string\n readonly format: ConfigFormat\n}\n\n/**\n * Parse config file content using the appropriate parser for the given format.\n *\n * @param options - Parse content options.\n * @returns A ConfigOperationResult with the parsed data or an error.\n */\nexport function parseContent(options: ParseContentOptions): ConfigOperationResult<unknown> {\n const { content, filePath, format } = options\n return match(format)\n .with('json', () => parseJson(content, filePath))\n .with('jsonc', () => parseJsoncContent(content, filePath))\n .with('yaml', () => parseYamlContent(content, filePath))\n .exhaustive()\n}\n\n/**\n * Serialize data to a string in the specified config format.\n *\n * @param data - The data to serialize.\n * @param format - The target config format.\n * @returns The serialized string representation.\n */\nexport function serializeContent(data: unknown, format: ConfigFormat): string {\n return match(format)\n .with('json', () => {\n const [, json] = jsonStringify(data, { pretty: true })\n return `${json}\\n`\n })\n .with('jsonc', () => {\n const [, json] = jsonStringify(data, { pretty: true })\n return `${json}\\n`\n })\n .with('yaml', () => stringifyYaml(data))\n .exhaustive()\n}\n\n/**\n * Get the file extension string for a given config format.\n *\n * @param format - The config format.\n * @returns The file extension including the leading dot (e.g. '.json').\n */\nexport function getExtension(format: ConfigFormat): string {\n return match(format)\n .with('json', () => '.json')\n .with('jsonc', () => '.jsonc')\n .with('yaml', () => '.yaml')\n .exhaustive()\n}\n","import { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\n\nimport { attemptAsync, err, match } from '@kidd-cli/utils/fp'\nimport { formatZodIssues } from '@kidd-cli/utils/validate'\nimport type { ZodTypeAny, output } from 'zod'\n\nimport { findConfig, getConfigFileNames } from './find.js'\nimport { getExtension, getFormat, parseContent, serializeContent } from './parse.js'\nimport type {\n Config,\n ConfigOperationResult,\n ConfigOptions,\n ConfigResult,\n ConfigWriteOptions,\n ConfigWriteResult,\n} from './types.js'\n\n/**\n * Create a typed config client that loads, validates, and writes config files.\n *\n * @param options - Config client options including name and Zod schema.\n * @returns A {@link Config} client instance.\n */\nexport function createConfigClient<TSchema extends ZodTypeAny>(\n options: ConfigOptions<TSchema>\n): Config<output<TSchema>> {\n const { name, schema, searchPaths } = options\n const fileNames = getConfigFileNames(name)\n\n /**\n * Find a config file in the given directory.\n *\n * @private\n * @param cwd - Working directory to search from.\n * @returns The path to the config file, or null if not found.\n */\n async function find(cwd?: string): Promise<string | null> {\n return findConfig({\n cwd: cwd ?? process.cwd(),\n fileNames,\n searchPaths,\n })\n }\n\n /**\n * Load and validate a config file.\n *\n * @private\n * @param cwd - Working directory to search from.\n * @returns A ConfigOperationResult with the loaded config, or [null, null] if not found.\n */\n async function load(\n cwd?: string\n ): Promise<ConfigOperationResult<ConfigResult<output<TSchema>>> | readonly [null, null]> {\n const filePath = await find(cwd)\n if (!filePath) {\n return [null, null]\n }\n\n const [readError, content] = await attemptAsync(() => readFile(filePath, 'utf8'))\n if (readError || content === null) {\n const errorDetail = resolveReadErrorDetail(readError)\n return err(`Failed to read config at ${filePath}: ${errorDetail}`)\n }\n\n const format = getFormat(filePath)\n const parsedResult = parseContent({ content, filePath, format })\n\n if (parsedResult[0]) {\n return [parsedResult[0], null]\n }\n\n const result = schema.safeParse(parsedResult[1])\n if (!result.success) {\n const { message } = formatZodIssues(result.error.issues, '\\n')\n return err(`Invalid config in ${filePath}:\\n${message}`)\n }\n\n return [\n null,\n {\n config: result.data,\n filePath,\n format,\n },\n ]\n }\n\n /**\n * Validate and write config data to a file.\n *\n * @private\n * @param data - The config data to write.\n * @param writeOptions - Write options including path and format.\n * @returns A ConfigOperationResult with the write result.\n */\n async function write(\n data: output<TSchema>,\n writeOptions: ConfigWriteOptions = {}\n ): Promise<ConfigOperationResult<ConfigWriteResult>> {\n const result = schema.safeParse(data)\n if (!result.success) {\n const { message } = formatZodIssues(result.error.issues, '\\n')\n return err(`Invalid config data:\\n${message}`)\n }\n\n const resolvedFormat = match(writeOptions)\n .when(\n (opts) => opts.format !== null && opts.format !== undefined,\n (opts) => opts.format ?? ('jsonc' as const)\n )\n .when(\n (opts) => opts.filePath !== null && opts.filePath !== undefined,\n (opts) => getFormat(opts.filePath ?? '')\n )\n .otherwise(() => 'jsonc' as const)\n\n const resolvedFilePath = match(writeOptions.filePath)\n .when(\n (fp) => fp !== null && fp !== undefined,\n (fp) => fp ?? ''\n )\n .otherwise(() => {\n const dir = writeOptions.dir ?? process.cwd()\n const ext = getExtension(resolvedFormat)\n return join(dir, `.${name}${ext}`)\n })\n\n const serialized = serializeContent(result.data, resolvedFormat)\n\n const [mkdirError] = await attemptAsync(() =>\n mkdir(dirname(resolvedFilePath), { recursive: true })\n )\n if (mkdirError) {\n return err(`Failed to create directory for ${resolvedFilePath}: ${String(mkdirError)}`)\n }\n\n const [writeError] = await attemptAsync(() => writeFile(resolvedFilePath, serialized, 'utf8'))\n if (writeError) {\n return err(`Failed to write config to ${resolvedFilePath}: ${String(writeError)}`)\n }\n\n return [null, { filePath: resolvedFilePath, format: resolvedFormat }]\n }\n\n return { find, load, write }\n}\n\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the error detail string from a read error.\n *\n * @private\n * @param readError - The error from the read operation, or null.\n * @returns A descriptive error string.\n */\nfunction resolveReadErrorDetail(readError: unknown): string {\n if (readError) {\n return String(readError)\n }\n return 'empty file'\n}\n"],"mappings":";;;;;;;;;;;;;AAQA,MAAa,oBAAoB;;;;ACFjC,MAAa,eAAe;AAC5B,MAAa,oBAAoB;CAAC;CAAU;CAAS;CAAQ;;;;;;;;;;;;;ACW7D,SAAgB,mBAAmB,MAAwB;AACzD,QAAO,kBAAkB,KAAK,QAAQ,IAAI,OAAO,MAAM;;;;;;;;AASzD,eAAsB,WAAW,UAAoC;CACnE,MAAM,CAAC,SAAS,MAAM,mBAAmB,OAAO,SAAS,CAAC;AAC1D,QAAO,CAAC;;;;;;;;;;;;AAaV,eAAsB,eACpB,KACA,WACwB;AAYxB,SAXgB,MAAM,QAAQ,IAC5B,UAAU,IAAI,OAAO,aAAa;EAChC,MAAM,WAAW,KAAK,KAAK,SAAS;AAEpC,MADe,MAAM,WAAW,SAAS,CAEvC,QAAO;AAET,SAAO;GACP,CACH,EACqB,MAAM,WAA6B,WAAW,KAAK,IACzD;;;;;;;;;;;;AAalB,eAAsB,WAAW,SAIN;CACzB,MAAM,EAAE,WAAW,KAAK,gBAAgB;AAExC,KAAI,aAAa;EAIf,MAAM,SAHgB,MAAM,QAAQ,IAClC,YAAY,KAAK,QAAQ,eAAe,KAAK,UAAU,CAAC,CACzD,EAC2B,MAAM,WAA6B,WAAW,KAAK;AAC/E,MAAI,MACF,QAAO;;CAIX,MAAM,UAAU,MAAM,eAAe,KAAK,UAAU;AACpD,KAAI,QACF,QAAO;CAGT,MAAM,cAAc,gBAAgB,IAAI;AACxC,KAAI,eAAe,YAAY,SAAS,KAAK;EAC3C,MAAM,WAAW,MAAM,eAAe,YAAY,MAAM,UAAU;AAClE,MAAI,SACF,QAAO;;AAIX,QAAO;;;;;;;;;;;ACnFT,SAAgB,UAAU,UAAgC;AAExD,QAAO,MADK,QAAQ,SAAS,CACZ,CACd,KAAK,gBAAgB,QAAiB,CACtC,KAAK,eAAe,OAAgB,CACpC,gBAAgB,OAAgB;;;;;;;;;AAUrC,SAAgB,UAAU,SAAiB,UAAkD;CAC3F,MAAM,CAAC,OAAO,UAAU,UAAU,QAAQ;AAC1C,KAAI,MACF,QAAO,IAAI,2BAA2B,SAAS,IAAI,MAAM,UAAU;AAErE,QAAO,CAAC,MAAM,OAAO;;;;;;;;;AAUvB,SAAgB,kBACd,SACA,UACgC;CAChC,MAAM,SAAuB,EAAE;CAC/B,MAAM,SAASA,MAAW,SAAS,QAAQ;EACzC,mBAAmB;EACnB,oBAAoB;EACrB,CAAC;AACF,KAAI,OAAO,SAAS,aAOlB,QAAO,IAAI,4BAA4B,SAAS,KAN1B,OACnB,KACE,eACC,OAAO,oBAAoB,WAAW,MAAM,CAAC,aAAa,WAAW,SACxE,CACA,KAAK,KAAK,GACwD;AAEvE,QAAO,CAAC,MAAM,OAAO;;;;;;;;;AAUvB,SAAgB,iBACd,SACA,UACgC;CAChC,MAAM,CAAC,OAAO,UAAU,cAAcC,QAAU,QAAQ,CAAC;AACzD,KAAI,MACF,QAAO,IAAI,2BAA2B,SAAS,IAAI,OAAO,MAAM,GAAG;AAErE,QAAO,CAAC,MAAM,OAAO;;;;;;;;AAkBvB,SAAgB,aAAa,SAA8D;CACzF,MAAM,EAAE,SAAS,UAAU,WAAW;AACtC,QAAO,MAAM,OAAO,CACjB,KAAK,cAAc,UAAU,SAAS,SAAS,CAAC,CAChD,KAAK,eAAe,kBAAkB,SAAS,SAAS,CAAC,CACzD,KAAK,cAAc,iBAAiB,SAAS,SAAS,CAAC,CACvD,YAAY;;;;;;;;;AAUjB,SAAgB,iBAAiB,MAAe,QAA8B;AAC5E,QAAO,MAAM,OAAO,CACjB,KAAK,cAAc;EAClB,MAAM,GAAG,QAAQ,cAAc,MAAM,EAAE,QAAQ,MAAM,CAAC;AACtD,SAAO,GAAG,KAAK;GACf,CACD,KAAK,eAAe;EACnB,MAAM,GAAG,QAAQ,cAAc,MAAM,EAAE,QAAQ,MAAM,CAAC;AACtD,SAAO,GAAG,KAAK;GACf,CACD,KAAK,cAAcC,UAAc,KAAK,CAAC,CACvC,YAAY;;;;;;;;AASjB,SAAgB,aAAa,QAA8B;AACzD,QAAO,MAAM,OAAO,CACjB,KAAK,cAAc,QAAQ,CAC3B,KAAK,eAAe,SAAS,CAC7B,KAAK,cAAc,QAAQ,CAC3B,YAAY;;;;;;;;;;;ACvHjB,SAAgB,mBACd,SACyB;CACzB,MAAM,EAAE,MAAM,QAAQ,gBAAgB;CACtC,MAAM,YAAY,mBAAmB,KAAK;;;;;;;;CAS1C,eAAe,KAAK,KAAsC;AACxD,SAAO,WAAW;GAChB,KAAK,OAAO,QAAQ,KAAK;GACzB;GACA;GACD,CAAC;;;;;;;;;CAUJ,eAAe,KACb,KACuF;EACvF,MAAM,WAAW,MAAM,KAAK,IAAI;AAChC,MAAI,CAAC,SACH,QAAO,CAAC,MAAM,KAAK;EAGrB,MAAM,CAAC,WAAW,WAAW,MAAM,mBAAmB,SAAS,UAAU,OAAO,CAAC;AACjF,MAAI,aAAa,YAAY,KAE3B,QAAO,IAAI,4BAA4B,SAAS,IAD5B,uBAAuB,UAAU,GACa;EAGpE,MAAM,SAAS,UAAU,SAAS;EAClC,MAAM,eAAe,aAAa;GAAE;GAAS;GAAU;GAAQ,CAAC;AAEhE,MAAI,aAAa,GACf,QAAO,CAAC,aAAa,IAAI,KAAK;EAGhC,MAAM,SAAS,OAAO,UAAU,aAAa,GAAG;AAChD,MAAI,CAAC,OAAO,SAAS;GACnB,MAAM,EAAE,YAAY,gBAAgB,OAAO,MAAM,QAAQ,KAAK;AAC9D,UAAO,IAAI,qBAAqB,SAAS,KAAK,UAAU;;AAG1D,SAAO,CACL,MACA;GACE,QAAQ,OAAO;GACf;GACA;GACD,CACF;;;;;;;;;;CAWH,eAAe,MACb,MACA,eAAmC,EAAE,EACc;EACnD,MAAM,SAAS,OAAO,UAAU,KAAK;AACrC,MAAI,CAAC,OAAO,SAAS;GACnB,MAAM,EAAE,YAAY,gBAAgB,OAAO,MAAM,QAAQ,KAAK;AAC9D,UAAO,IAAI,yBAAyB,UAAU;;EAGhD,MAAM,iBAAiB,MAAM,aAAa,CACvC,MACE,SAAS,KAAK,WAAW,QAAQ,KAAK,WAAW,SACjD,SAAS,KAAK,UAAW,QAC3B,CACA,MACE,SAAS,KAAK,aAAa,QAAQ,KAAK,aAAa,SACrD,SAAS,UAAU,KAAK,YAAY,GAAG,CACzC,CACA,gBAAgB,QAAiB;EAEpC,MAAM,mBAAmB,MAAM,aAAa,SAAS,CAClD,MACE,OAAO,OAAO,QAAQ,OAAO,SAC7B,OAAO,MAAM,GACf,CACA,gBAAgB;AAGf,UAAO,KAFK,aAAa,OAAO,QAAQ,KAAK,EAE5B,IAAI,OADT,aAAa,eAAe,GACN;IAClC;EAEJ,MAAM,aAAa,iBAAiB,OAAO,MAAM,eAAe;EAEhE,MAAM,CAAC,cAAc,MAAM,mBACzB,MAAM,QAAQ,iBAAiB,EAAE,EAAE,WAAW,MAAM,CAAC,CACtD;AACD,MAAI,WACF,QAAO,IAAI,kCAAkC,iBAAiB,IAAI,OAAO,WAAW,GAAG;EAGzF,MAAM,CAAC,cAAc,MAAM,mBAAmB,UAAU,kBAAkB,YAAY,OAAO,CAAC;AAC9F,MAAI,WACF,QAAO,IAAI,6BAA6B,iBAAiB,IAAI,OAAO,WAAW,GAAG;AAGpF,SAAO,CAAC,MAAM;GAAE,UAAU;GAAkB,QAAQ;GAAgB,CAAC;;AAGvE,QAAO;EAAE;EAAM;EAAM;EAAO;;;;;;;;;AAY9B,SAAS,uBAAuB,WAA4B;AAC1D,KAAI,UACF,QAAO,OAAO,UAAU;AAE1B,QAAO"}