@kidd-cli/core 0.4.0 → 0.5.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 (46) hide show
  1. package/README.md +21 -5
  2. package/dist/{config-D8e5qxLp.js → config-BiEi8RG2.js} +2 -2
  3. package/dist/{config-D8e5qxLp.js.map → config-BiEi8RG2.js.map} +1 -1
  4. package/dist/{create-store-OHdkm_Yt.js → create-store-CGeHrTcl.js} +2 -2
  5. package/dist/{create-store-OHdkm_Yt.js.map → create-store-CGeHrTcl.js.map} +1 -1
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +37 -56
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/config.js +2 -2
  11. package/dist/lib/format.d.ts +73 -0
  12. package/dist/lib/format.d.ts.map +1 -0
  13. package/dist/lib/format.js +20 -0
  14. package/dist/lib/format.js.map +1 -0
  15. package/dist/lib/logger.d.ts +1 -1
  16. package/dist/lib/logger.js +10 -0
  17. package/dist/lib/logger.js.map +1 -1
  18. package/dist/lib/project.d.ts +1 -1
  19. package/dist/lib/project.js +1 -1
  20. package/dist/lib/store.d.ts +1 -1
  21. package/dist/lib/store.js +2 -2
  22. package/dist/{logger-9j49T5da.d.ts → logger-Bm-LRSeQ.d.ts} +17 -1
  23. package/dist/logger-Bm-LRSeQ.d.ts.map +1 -0
  24. package/dist/middleware/auth.d.ts +1 -1
  25. package/dist/middleware/auth.js +3 -3
  26. package/dist/middleware/http.d.ts +1 -1
  27. package/dist/middleware/http.js +1 -1
  28. package/dist/middleware/icons.d.ts +119 -0
  29. package/dist/middleware/icons.d.ts.map +1 -0
  30. package/dist/middleware/icons.js +824 -0
  31. package/dist/middleware/icons.js.map +1 -0
  32. package/dist/{middleware-BWnPSRWR.js → middleware-BewRXb2G.js} +1 -1
  33. package/dist/{middleware-BWnPSRWR.js.map → middleware-BewRXb2G.js.map} +1 -1
  34. package/dist/{project-D0g84bZY.js → project-CoWHMVc8.js} +1 -1
  35. package/dist/{project-D0g84bZY.js.map → project-CoWHMVc8.js.map} +1 -1
  36. package/dist/tally-ioa20iGw.js +220 -0
  37. package/dist/tally-ioa20iGw.js.map +1 -0
  38. package/dist/{types-D-BxshYM.d.ts → types-Boe_1EjY.d.ts} +1 -1
  39. package/dist/{types-D-BxshYM.d.ts.map → types-Boe_1EjY.d.ts.map} +1 -1
  40. package/dist/types-Cp8_uIil.d.ts +160 -0
  41. package/dist/types-Cp8_uIil.d.ts.map +1 -0
  42. package/dist/{types-U73X_oQ_.d.ts → types-s-yUj9Zj.d.ts} +47 -37
  43. package/dist/types-s-yUj9Zj.d.ts.map +1 -0
  44. package/package.json +10 -1
  45. package/dist/logger-9j49T5da.d.ts.map +0 -1
  46. package/dist/types-U73X_oQ_.d.ts.map +0 -1
package/README.md CHANGED
@@ -133,17 +133,33 @@ log.success('Complete')
133
133
  log.outro('Done')
134
134
  ```
135
135
 
136
- ### `kidd/output`
136
+ ### `kidd/format`
137
137
 
138
- Structured output for JSON, templates, and files.
138
+ Standalone pure format functions for terminal output styling.
139
139
 
140
140
  ```ts
141
- import { output } from '@kidd-cli/core/output'
141
+ import { formatCheck, formatFinding, formatTally, formatDuration } from '@kidd-cli/core/format'
142
142
 
143
- output.json({ status: 'ok' })
144
- output.write({ path: './out.json', content: output.toJson(data) })
143
+ console.log(formatCheck({ status: 'pass', name: 'src/auth.test.ts', duration: 42 }))
144
+ console.log(formatFinding({ severity: 'error', rule: 'no-unused-vars', message: '...' }))
145
+ console.log(formatTally({ style: 'inline', stats: ['1 error', '3 warnings'] }))
146
+ console.log(formatDuration(1234)) // "1.23s"
145
147
  ```
146
148
 
149
+ #### Migration from `ctx.output`
150
+
151
+ If you previously used the `ctx.output` API, migrate to the new methods:
152
+
153
+ | Before | After |
154
+ | ------------------------- | ---------------------- |
155
+ | `ctx.output.result()` | `ctx.logger.check()` |
156
+ | `ctx.output.diagnostic()` | `ctx.logger.finding()` |
157
+ | `ctx.output.summary()` | `ctx.logger.tally()` |
158
+
159
+ Format function renames: `formatResult` → `formatCheck`, `formatDiagnostic` → `formatFinding`, `formatSummary` → `formatTally`
160
+
161
+ Type renames: `ResultInput` → `CheckInput`, `DiagnosticInput` → `FindingInput`, `SummaryInput` → `TallyInput`
162
+
147
163
  ### `kidd/errors`
148
164
 
149
165
  Error creation, formatting, and sanitization utilities.
@@ -1,4 +1,4 @@
1
- import { i as findProjectRoot } from "./project-D0g84bZY.js";
1
+ import { i as findProjectRoot } from "./project-CoWHMVc8.js";
2
2
  import { dirname, extname, join } from "node:path";
3
3
  import { attempt, attemptAsync, err, match } from "@kidd-cli/utils/fp";
4
4
  import { jsonParse, jsonStringify } from "@kidd-cli/utils/json";
@@ -262,4 +262,4 @@ function resolveReadErrorDetail(readError) {
262
262
  //#endregion
263
263
  export { createConfigClient as t };
264
264
 
265
- //# sourceMappingURL=config-D8e5qxLp.js.map
265
+ //# sourceMappingURL=config-BiEi8RG2.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config-D8e5qxLp.js","names":["stringifyYaml","parseJsonc","parseYaml"],"sources":["../src/lib/config/constants.ts","../src/lib/config/find.ts","../src/lib/config/parse.ts","../src/lib/config/create-config.ts"],"sourcesContent":["/**\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 { join } from 'node:path'\n\nimport { fileExists } from '@kidd-cli/utils/fs'\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 * 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\n// ---------------------------------------------------------------------------\n// Private helpers\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 * @private\n */\nasync function findConfigFile(dir: string, fileNames: readonly string[]): 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","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 * 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\n// ---------------------------------------------------------------------------\n// Private helpers\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 * @private\n */\nfunction 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 * @private\n */\nfunction parseJsoncContent(content: string, filePath: string): ConfigOperationResult<unknown> {\n // Intentional mutation: jsonc-parser API requires a mutable errors array.\n // There is no immutable alternative — the parser populates it during parsing.\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 * @private\n */\nfunction parseYamlContent(content: string, filePath: string): 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","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":"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"}
1
+ {"version":3,"file":"config-BiEi8RG2.js","names":["stringifyYaml","parseJsonc","parseYaml"],"sources":["../src/lib/config/constants.ts","../src/lib/config/find.ts","../src/lib/config/parse.ts","../src/lib/config/create-config.ts"],"sourcesContent":["/**\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 { join } from 'node:path'\n\nimport { fileExists } from '@kidd-cli/utils/fs'\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 * 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\n// ---------------------------------------------------------------------------\n// Private helpers\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 * @private\n */\nasync function findConfigFile(dir: string, fileNames: readonly string[]): 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","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 * 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\n// ---------------------------------------------------------------------------\n// Private helpers\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 * @private\n */\nfunction 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 * @private\n */\nfunction parseJsoncContent(content: string, filePath: string): ConfigOperationResult<unknown> {\n // Intentional mutation: jsonc-parser API requires a mutable errors array.\n // There is no immutable alternative — the parser populates it during parsing.\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 * @private\n */\nfunction parseYamlContent(content: string, filePath: string): 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","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":"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"}
@@ -1,4 +1,4 @@
1
- import { n as resolveLocalPath, t as resolveGlobalPath } from "./project-D0g84bZY.js";
1
+ import { n as resolveLocalPath, t as resolveGlobalPath } from "./project-CoWHMVc8.js";
2
2
  import { join } from "node:path";
3
3
  import { attempt, err, match, ok } from "@kidd-cli/utils/fp";
4
4
  import { jsonParse, jsonStringify } from "@kidd-cli/utils/json";
@@ -221,4 +221,4 @@ function resolveSaveDir(options) {
221
221
  //#endregion
222
222
  export { createStore as t };
223
223
 
224
- //# sourceMappingURL=create-store-OHdkm_Yt.js.map
224
+ //# sourceMappingURL=create-store-CGeHrTcl.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-store-OHdkm_Yt.js","names":[],"sources":["../src/lib/store/create-store.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport { attempt, err, match, ok } from '@kidd-cli/utils/fp'\nimport type { Result } from '@kidd-cli/utils/fp'\nimport { jsonParse, jsonStringify } from '@kidd-cli/utils/json'\n\nimport { resolveGlobalPath, resolveLocalPath } from '@/lib/project/index.js'\nimport type { PathSource } from '@/lib/project/types.js'\n\nimport type { FileStore, LoadOptions, SaveOptions, StoreOptions } from './types.js'\n\n/**\n * Create a file-backed {@link FileStore} that resolves JSON files from project-local\n * or global home directories.\n *\n * @param options - Store configuration.\n * @returns A FileStore instance.\n */\nexport function createStore<TData = unknown>(options: StoreOptions<TData>): FileStore<TData> {\n const { dirName, defaults } = options\n\n /**\n * Resolve the local project directory for the store.\n *\n * @private\n * @param startDir - Optional directory to start searching from.\n * @returns The local directory path, or null if no project root is found.\n */\n function getLocalDir(startDir?: string): string | null {\n return resolveLocalPath({ dirName, startDir })\n }\n\n /**\n * Resolve the global home directory for the store.\n *\n * @private\n * @returns The global directory path.\n */\n function getGlobalDir(): string {\n return resolveGlobalPath({ dirName })\n }\n\n /**\n * Read the raw string content from a file path.\n *\n * @private\n * @param filePath - The file path to read.\n * @returns The file content, or null if the file does not exist or cannot be read.\n */\n function loadFromPath(filePath: string): string | null {\n const [error, content] = attempt(() => readFileSync(filePath, 'utf8'))\n\n if (error) {\n return null\n }\n\n return content\n }\n\n /**\n * Resolve a file from local or global directories based on the source strategy.\n *\n * @private\n * @param resolveOptions - Resolution options.\n * @returns The resolved result, or null if not found.\n */\n function resolveFromSource<T>(resolveOptions: {\n source: PathSource\n localDir: string | null\n globalDir: string\n filename: string\n handler: (filePath: string) => T | null\n }): T | null {\n return match(resolveOptions.source)\n .with('local', (): T | null => {\n if (!resolveOptions.localDir) {\n return null\n }\n return resolveOptions.handler(join(resolveOptions.localDir, resolveOptions.filename))\n })\n .with('global', () =>\n resolveOptions.handler(join(resolveOptions.globalDir, resolveOptions.filename))\n )\n .with('resolve', (): T | null => {\n if (resolveOptions.localDir) {\n const localResult = resolveOptions.handler(\n join(resolveOptions.localDir, resolveOptions.filename)\n )\n if (localResult !== null) {\n return localResult\n }\n }\n return resolveOptions.handler(join(resolveOptions.globalDir, resolveOptions.filename))\n })\n .exhaustive()\n }\n\n /**\n * Load the raw string content of a store file.\n *\n * @private\n * @param filename - The filename to load.\n * @param loadOptions - Options controlling source resolution.\n * @returns The raw file content, or null if not found.\n */\n function loadRaw(filename: string, loadOptions: LoadOptions = {}): string | null {\n const { source: loadSource = 'resolve', startDir } = loadOptions\n const localDir = getLocalDir(startDir)\n const globalDir = getGlobalDir()\n\n return resolveFromSource<string>({\n filename,\n globalDir,\n handler: loadFromPath,\n localDir,\n source: loadSource,\n })\n }\n\n /**\n * Load and parse a store file as JSON, merging with defaults if available.\n *\n * @private\n * @param filename - The filename to load.\n * @param loadOptions - Options controlling source resolution.\n * @returns The parsed data, defaults, or null.\n */\n function load(filename: string, loadOptions: LoadOptions = {}): TData | null {\n const raw = loadRaw(filename, loadOptions)\n\n if (raw === null) {\n return defaults ?? null\n }\n\n const [parseError, parsed] = jsonParse(raw)\n if (parseError) {\n return defaults ?? null\n }\n\n if (defaults) {\n return { ...defaults, ...(parsed as Partial<TData>) }\n }\n return parsed as TData\n }\n\n /**\n * Check if a file exists at the given path and return the path if so.\n *\n * @private\n * @param filePath - The file path to check.\n * @returns The file path if it exists, or null.\n */\n function checkFileExists(filePath: string): string | null {\n if (existsSync(filePath)) {\n return filePath\n }\n return null\n }\n\n /**\n * Resolve the file path for a store file without reading its content.\n *\n * @private\n * @param filename - The filename to resolve.\n * @param loadOptions - Options controlling source resolution.\n * @returns The resolved file path, or null if not found.\n */\n function getFilePath(filename: string, loadOptions: LoadOptions = {}): string | null {\n const { source: fileSource = 'resolve', startDir } = loadOptions\n const localDir = getLocalDir(startDir)\n const globalDir = getGlobalDir()\n\n return resolveFromSource<string>({\n filename,\n globalDir,\n handler: checkFileExists,\n localDir,\n source: fileSource,\n })\n }\n\n /**\n * Serialize data to JSON and write it to a store file.\n *\n * Creates the target directory if it does not exist. Defaults to\n * the global home directory when no source is specified.\n *\n * @private\n * @param filename - The filename to write.\n * @param data - The data to serialize.\n * @param saveOptions - Options controlling the write target.\n * @returns A Result with the written file path on success.\n */\n function save(filename: string, data: unknown, saveOptions: SaveOptions = {}): Result<string> {\n const { source: saveSource = 'global', startDir } = saveOptions\n\n const dir = resolveSaveDir({\n globalDir: getGlobalDir(),\n localDir: getLocalDir(startDir),\n source: saveSource,\n })\n\n if (dir === null) {\n return err(new Error(`Cannot save to \"${saveSource}\" — no local project directory found`))\n }\n\n const [stringifyError, json] = jsonStringify(data, { pretty: true })\n\n if (stringifyError) {\n return err(stringifyError)\n }\n\n const filePath = join(dir, filename)\n\n const [writeError] = attempt(() => {\n mkdirSync(dir, { mode: 0o700, recursive: true })\n writeFileSync(filePath, json, { encoding: 'utf8', mode: 0o600 })\n })\n\n if (writeError) {\n return err(writeError)\n }\n\n return ok(filePath)\n }\n\n /**\n * Remove a file from the store.\n *\n * Returns `ok(filePath)` when the file was deleted or did not exist\n * (idempotent). Returns an error when the target directory cannot be\n * resolved or the unlink fails.\n *\n * @private\n * @param filename - The filename to remove.\n * @param removeOptions - Options controlling the removal target.\n * @returns A Result with the file path on success.\n */\n function remove(filename: string, removeOptions: SaveOptions = {}): Result<string> {\n const { source: removeSource = 'global', startDir } = removeOptions\n\n const dir = resolveSaveDir({\n globalDir: getGlobalDir(),\n localDir: getLocalDir(startDir),\n source: removeSource,\n })\n\n if (dir === null) {\n return err(\n new Error(`Cannot remove from \"${removeSource}\" — no local project directory found`)\n )\n }\n\n const filePath = join(dir, filename)\n\n if (!existsSync(filePath)) {\n return ok(filePath)\n }\n\n const [removeError] = attempt(() => {\n unlinkSync(filePath)\n })\n\n if (removeError) {\n return err(removeError)\n }\n\n return ok(filePath)\n }\n\n return {\n getFilePath,\n getGlobalDir,\n getLocalDir,\n load,\n loadRaw,\n remove,\n save,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the target directory for a save operation.\n *\n * @private\n * @param options - Resolution options.\n * @returns The directory path, or null when `local` is requested but unavailable.\n */\nfunction resolveSaveDir(options: {\n readonly localDir: string | null\n readonly globalDir: string\n readonly source: 'local' | 'global'\n}): string | null {\n return match(options.source)\n .with('local', (): string | null => options.localDir)\n .with('global', () => options.globalDir)\n .exhaustive()\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-store-CGeHrTcl.js","names":[],"sources":["../src/lib/store/create-store.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport { attempt, err, match, ok } from '@kidd-cli/utils/fp'\nimport type { Result } from '@kidd-cli/utils/fp'\nimport { jsonParse, jsonStringify } from '@kidd-cli/utils/json'\n\nimport { resolveGlobalPath, resolveLocalPath } from '@/lib/project/index.js'\nimport type { PathSource } from '@/lib/project/types.js'\n\nimport type { FileStore, LoadOptions, SaveOptions, StoreOptions } from './types.js'\n\n/**\n * Create a file-backed {@link FileStore} that resolves JSON files from project-local\n * or global home directories.\n *\n * @param options - Store configuration.\n * @returns A FileStore instance.\n */\nexport function createStore<TData = unknown>(options: StoreOptions<TData>): FileStore<TData> {\n const { dirName, defaults } = options\n\n /**\n * Resolve the local project directory for the store.\n *\n * @private\n * @param startDir - Optional directory to start searching from.\n * @returns The local directory path, or null if no project root is found.\n */\n function getLocalDir(startDir?: string): string | null {\n return resolveLocalPath({ dirName, startDir })\n }\n\n /**\n * Resolve the global home directory for the store.\n *\n * @private\n * @returns The global directory path.\n */\n function getGlobalDir(): string {\n return resolveGlobalPath({ dirName })\n }\n\n /**\n * Read the raw string content from a file path.\n *\n * @private\n * @param filePath - The file path to read.\n * @returns The file content, or null if the file does not exist or cannot be read.\n */\n function loadFromPath(filePath: string): string | null {\n const [error, content] = attempt(() => readFileSync(filePath, 'utf8'))\n\n if (error) {\n return null\n }\n\n return content\n }\n\n /**\n * Resolve a file from local or global directories based on the source strategy.\n *\n * @private\n * @param resolveOptions - Resolution options.\n * @returns The resolved result, or null if not found.\n */\n function resolveFromSource<T>(resolveOptions: {\n source: PathSource\n localDir: string | null\n globalDir: string\n filename: string\n handler: (filePath: string) => T | null\n }): T | null {\n return match(resolveOptions.source)\n .with('local', (): T | null => {\n if (!resolveOptions.localDir) {\n return null\n }\n return resolveOptions.handler(join(resolveOptions.localDir, resolveOptions.filename))\n })\n .with('global', () =>\n resolveOptions.handler(join(resolveOptions.globalDir, resolveOptions.filename))\n )\n .with('resolve', (): T | null => {\n if (resolveOptions.localDir) {\n const localResult = resolveOptions.handler(\n join(resolveOptions.localDir, resolveOptions.filename)\n )\n if (localResult !== null) {\n return localResult\n }\n }\n return resolveOptions.handler(join(resolveOptions.globalDir, resolveOptions.filename))\n })\n .exhaustive()\n }\n\n /**\n * Load the raw string content of a store file.\n *\n * @private\n * @param filename - The filename to load.\n * @param loadOptions - Options controlling source resolution.\n * @returns The raw file content, or null if not found.\n */\n function loadRaw(filename: string, loadOptions: LoadOptions = {}): string | null {\n const { source: loadSource = 'resolve', startDir } = loadOptions\n const localDir = getLocalDir(startDir)\n const globalDir = getGlobalDir()\n\n return resolveFromSource<string>({\n filename,\n globalDir,\n handler: loadFromPath,\n localDir,\n source: loadSource,\n })\n }\n\n /**\n * Load and parse a store file as JSON, merging with defaults if available.\n *\n * @private\n * @param filename - The filename to load.\n * @param loadOptions - Options controlling source resolution.\n * @returns The parsed data, defaults, or null.\n */\n function load(filename: string, loadOptions: LoadOptions = {}): TData | null {\n const raw = loadRaw(filename, loadOptions)\n\n if (raw === null) {\n return defaults ?? null\n }\n\n const [parseError, parsed] = jsonParse(raw)\n if (parseError) {\n return defaults ?? null\n }\n\n if (defaults) {\n return { ...defaults, ...(parsed as Partial<TData>) }\n }\n return parsed as TData\n }\n\n /**\n * Check if a file exists at the given path and return the path if so.\n *\n * @private\n * @param filePath - The file path to check.\n * @returns The file path if it exists, or null.\n */\n function checkFileExists(filePath: string): string | null {\n if (existsSync(filePath)) {\n return filePath\n }\n return null\n }\n\n /**\n * Resolve the file path for a store file without reading its content.\n *\n * @private\n * @param filename - The filename to resolve.\n * @param loadOptions - Options controlling source resolution.\n * @returns The resolved file path, or null if not found.\n */\n function getFilePath(filename: string, loadOptions: LoadOptions = {}): string | null {\n const { source: fileSource = 'resolve', startDir } = loadOptions\n const localDir = getLocalDir(startDir)\n const globalDir = getGlobalDir()\n\n return resolveFromSource<string>({\n filename,\n globalDir,\n handler: checkFileExists,\n localDir,\n source: fileSource,\n })\n }\n\n /**\n * Serialize data to JSON and write it to a store file.\n *\n * Creates the target directory if it does not exist. Defaults to\n * the global home directory when no source is specified.\n *\n * @private\n * @param filename - The filename to write.\n * @param data - The data to serialize.\n * @param saveOptions - Options controlling the write target.\n * @returns A Result with the written file path on success.\n */\n function save(filename: string, data: unknown, saveOptions: SaveOptions = {}): Result<string> {\n const { source: saveSource = 'global', startDir } = saveOptions\n\n const dir = resolveSaveDir({\n globalDir: getGlobalDir(),\n localDir: getLocalDir(startDir),\n source: saveSource,\n })\n\n if (dir === null) {\n return err(new Error(`Cannot save to \"${saveSource}\" — no local project directory found`))\n }\n\n const [stringifyError, json] = jsonStringify(data, { pretty: true })\n\n if (stringifyError) {\n return err(stringifyError)\n }\n\n const filePath = join(dir, filename)\n\n const [writeError] = attempt(() => {\n mkdirSync(dir, { mode: 0o700, recursive: true })\n writeFileSync(filePath, json, { encoding: 'utf8', mode: 0o600 })\n })\n\n if (writeError) {\n return err(writeError)\n }\n\n return ok(filePath)\n }\n\n /**\n * Remove a file from the store.\n *\n * Returns `ok(filePath)` when the file was deleted or did not exist\n * (idempotent). Returns an error when the target directory cannot be\n * resolved or the unlink fails.\n *\n * @private\n * @param filename - The filename to remove.\n * @param removeOptions - Options controlling the removal target.\n * @returns A Result with the file path on success.\n */\n function remove(filename: string, removeOptions: SaveOptions = {}): Result<string> {\n const { source: removeSource = 'global', startDir } = removeOptions\n\n const dir = resolveSaveDir({\n globalDir: getGlobalDir(),\n localDir: getLocalDir(startDir),\n source: removeSource,\n })\n\n if (dir === null) {\n return err(\n new Error(`Cannot remove from \"${removeSource}\" — no local project directory found`)\n )\n }\n\n const filePath = join(dir, filename)\n\n if (!existsSync(filePath)) {\n return ok(filePath)\n }\n\n const [removeError] = attempt(() => {\n unlinkSync(filePath)\n })\n\n if (removeError) {\n return err(removeError)\n }\n\n return ok(filePath)\n }\n\n return {\n getFilePath,\n getGlobalDir,\n getLocalDir,\n load,\n loadRaw,\n remove,\n save,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the target directory for a save operation.\n *\n * @private\n * @param options - Resolution options.\n * @returns The directory path, or null when `local` is requested but unavailable.\n */\nfunction resolveSaveDir(options: {\n readonly localDir: string | null\n readonly globalDir: string\n readonly source: 'local' | 'global'\n}): string | null {\n return match(options.source)\n .with('local', (): string | null => options.localDir)\n .with('global', () => options.globalDir)\n .exhaustive()\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { a as Command, c as CommandsConfig, d as MiddlewareEnv, f as MiddlewareFn, i as CliOptions, l as InferVariables, n as AutoloadOptions, o as CommandDef, p as Context, r as CliHelpOptions, s as CommandMap, t as ArgsDef, u as Middleware } from "./types-U73X_oQ_.js";
1
+ import { a as CliOptions, c as CommandMap, d as InferVariables, f as Middleware, h as Context, i as CliHelpOptions, l as CommandsConfig, m as MiddlewareFn, n as AutoloadOptions, o as Command, p as MiddlewareEnv, r as CliConfig, s as CommandDef, t as ArgsDef, u as ConfigType } from "./types-s-yUj9Zj.js";
2
2
  import { z } from "zod";
3
3
  import { defineConfig } from "@kidd-cli/config";
4
+ import { Colors } from "picocolors/types";
4
5
 
5
6
  //#region src/cli.d.ts
6
7
  /**
@@ -117,5 +118,5 @@ declare function decorateContext<TKey extends string, TValue>(ctx: Context, key:
117
118
  */
118
119
  declare function middleware<TEnv extends MiddlewareEnv = MiddlewareEnv>(handler: MiddlewareFn<TEnv>): Middleware<TEnv>;
119
120
  //#endregion
120
- export { type CliHelpOptions, type Command, type CommandsConfig, type Context, type MiddlewareEnv, autoload, cli, command, compose, decorateContext, defineConfig, middleware };
121
+ export { type CliConfig, type CliHelpOptions, type Colors, type Command, type CommandsConfig, type ConfigType, type Context, type MiddlewareEnv, autoload, cli, command, compose, decorateContext, defineConfig, middleware };
121
122
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/cli.ts","../src/command.ts","../src/compose.ts","../src/autoloader.ts","../src/context/decorate.ts","../src/middleware.ts"],"mappings":";;;;;;;;AA4BA;;;;;iBAAsB,GAAA,iBAAoB,CAAA,CAAE,OAAA,GAAU,CAAA,CAAE,OAAA,CAAA,CACtD,OAAA,EAAS,UAAA,CAAW,OAAA,IACnB,OAAA;;;;;;;;;;;;;;;;;iBCgBa,OAAA,kBACG,OAAA,GAAU,OAAA,kBACX,MAAA,oBAA0B,MAAA,sDACP,UAAA,CAAW,aAAA,eACnC,UAAA,CAAW,aAAA,IAAA,CACtB,GAAA,EAAK,UAAA,CAAW,QAAA,EAAU,OAAA,EAAS,WAAA,IAAe,OAAA;;;;;;;ADvBpD;UEdU,WAAA,8BAAyC,UAAA,CAAW,aAAA;EAAA,SACnD,SAAA,EAAW,cAAA,CAAe,WAAA,oCACf,CAAA,GACd,MAAA,oBACA,CAAA;AAAA;;;;;;;;;;;;;;;;;;;iBAsBQ,OAAA,oCAA2C,UAAA,CAAW,aAAA,IAAA,CACpE,WAAA,EAAa,WAAA,GACZ,UAAA,CAAW,WAAA,CAAY,WAAA;;;;;;;AFd1B;;iBGVsB,QAAA,CAAS,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,UAAA;;;;;;;AHUnE;;;;;;;;;;;;;;;;;;;;;;iBIAgB,eAAA,6BAAA,CACd,GAAA,EAAK,OAAA,EACL,GAAA,EAAK,IAAA,EACL,KAAA,EAAO,MAAA,GACN,OAAA;;;;;;;AJJH;;;;;;;;;;;;;iBKPgB,UAAA,cAAwB,aAAA,GAAgB,aAAA,CAAA,CACtD,OAAA,EAAS,YAAA,CAAa,IAAA,IACrB,UAAA,CAAW,IAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/cli.ts","../src/command.ts","../src/compose.ts","../src/autoloader.ts","../src/context/decorate.ts","../src/middleware.ts"],"mappings":";;;;;;;;;;AA4BA;;;;iBAAsB,GAAA,iBAAoB,CAAA,CAAE,OAAA,GAAU,CAAA,CAAE,OAAA,CAAA,CACtD,OAAA,EAAS,UAAA,CAAW,OAAA,IACnB,OAAA;;;;;;;;;;;;;;;;;iBCgBa,OAAA,kBACG,OAAA,GAAU,OAAA,kBACX,MAAA,oBAA0B,MAAA,sDACP,UAAA,CAAW,aAAA,eACnC,UAAA,CAAW,aAAA,IAAA,CACtB,GAAA,EAAK,UAAA,CAAW,QAAA,EAAU,OAAA,EAAS,WAAA,IAAe,OAAA;;;;;;;;UCrC1C,WAAA,8BAAyC,UAAA,CAAW,aAAA;EAAA,SACnD,SAAA,EAAW,cAAA,CAAe,WAAA,oCACf,CAAA,GACd,MAAA,oBACA,CAAA;AAAA;;;;;;;;;;;;;;;;;;;iBAsBQ,OAAA,oCAA2C,UAAA,CAAW,aAAA,IAAA,CACpE,WAAA,EAAa,WAAA,GACZ,UAAA,CAAW,WAAA,CAAY,WAAA;;;;;;;;AFd1B;iBGVsB,QAAA,CAAS,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,UAAA;;;;;;;;AHUnE;;;;;;;;;;;;;;;;;;;;;iBIAgB,eAAA,6BAAA,CACd,GAAA,EAAK,OAAA,EACL,GAAA,EAAK,IAAA,EACL,KAAA,EAAO,MAAA,GACN,OAAA;;;;;;;;AJJH;;;;;;;;;;;;iBKPgB,UAAA,cAAwB,aAAA,GAAgB,aAAA,CAAA,CACtD,OAAA,EAAS,YAAA,CAAa,IAAA,IACrB,UAAA,CAAW,IAAA"}
package/dist/index.js CHANGED
@@ -1,18 +1,20 @@
1
+ import "./tally-ioa20iGw.js";
1
2
  import { createCliLogger } from "./lib/logger.js";
2
- import { n as decorateContext, t as middleware } from "./middleware-BWnPSRWR.js";
3
- import "./project-D0g84bZY.js";
4
- import { t as createConfigClient } from "./config-D8e5qxLp.js";
3
+ import { n as decorateContext, t as middleware } from "./middleware-BewRXb2G.js";
4
+ import "./project-CoWHMVc8.js";
5
+ import { t as createConfigClient } from "./config-BiEi8RG2.js";
5
6
  import { basename, extname, join, resolve } from "node:path";
6
7
  import { loadConfig } from "@kidd-cli/config/loader";
7
8
  import { P, attemptAsync, err, isPlainObject, isString, match, ok } from "@kidd-cli/utils/fp";
8
9
  import yargs from "yargs";
9
10
  import { z } from "zod";
10
11
  import * as clack from "@clack/prompts";
12
+ import pc from "picocolors";
13
+ import { match as match$1 } from "ts-pattern";
11
14
  import { TAG, hasTag, withTag } from "@kidd-cli/utils/tag";
12
15
  import { jsonStringify } from "@kidd-cli/utils/json";
13
16
  import { readdir } from "node:fs/promises";
14
17
  import { formatZodIssues } from "@kidd-cli/utils/validate";
15
- import { match as match$1 } from "ts-pattern";
16
18
  import { defineConfig } from "@kidd-cli/config";
17
19
  //#region src/context/error.ts
18
20
  /**
@@ -76,40 +78,26 @@ function createContextErrorData(message, options) {
76
78
  }, "ContextError");
77
79
  }
78
80
  //#endregion
79
- //#region src/context/output.ts
81
+ //#region src/context/format.ts
80
82
  /**
81
- * Create the structured output methods for a context.
83
+ * Create the pure string formatter methods for a context.
82
84
  *
83
85
  * @private
84
- * @param stream - The writable stream to write output to.
85
- * @returns An Output instance backed by the given stream.
86
+ * @returns A Format instance with json and table formatters.
86
87
  */
87
- function createContextOutput(stream) {
88
- return {
89
- markdown(content) {
90
- stream.write(`${content}\n`);
88
+ function createContextFormat() {
89
+ return Object.freeze({
90
+ json(data) {
91
+ const [, json] = jsonStringify(data, { pretty: true });
92
+ return `${json}\n`;
91
93
  },
92
- raw(content) {
93
- stream.write(content);
94
- },
95
- table(rows, options) {
96
- if (options && options.json) {
97
- const [, json] = jsonStringify(rows, { pretty: true });
98
- stream.write(`${json}\n`);
99
- return;
100
- }
101
- if (rows.length === 0) return;
94
+ table(rows) {
95
+ if (rows.length === 0) return "";
102
96
  const [firstRow] = rows;
103
- if (!firstRow) return;
104
- writeTableToStream(stream, rows, Object.keys(firstRow));
105
- },
106
- write(data, options) {
107
- if (options && options.json || typeof data === "object" && data !== null) {
108
- const [, json] = jsonStringify(data, { pretty: true });
109
- stream.write(`${json}\n`);
110
- } else stream.write(`${String(data)}\n`);
97
+ if (!firstRow) return "";
98
+ return formatTable(rows, Object.keys(firstRow));
111
99
  }
112
- };
100
+ });
113
101
  }
114
102
  /**
115
103
  * Format an unknown value as a string for table cell display.
@@ -168,16 +156,16 @@ function computeColumnWidths(rows, keys) {
168
156
  });
169
157
  }
170
158
  /**
171
- * Write a formatted table (header, separator, rows) to a writable stream.
159
+ * Format a table (header, separator, rows) as a string.
172
160
  *
173
161
  * @private
174
- * @param stream - The writable stream.
175
162
  * @param rows - The data rows.
176
163
  * @param keys - The column keys.
164
+ * @returns The formatted table string.
177
165
  */
178
- function writeTableToStream(stream, rows, keys) {
166
+ function formatTable(rows, keys) {
179
167
  const widths = computeColumnWidths(rows, keys);
180
- const content = [
168
+ return `${[
181
169
  createTableHeader({
182
170
  keys,
183
171
  widths
@@ -188,8 +176,7 @@ function writeTableToStream(stream, rows, keys) {
188
176
  row,
189
177
  widths
190
178
  }))
191
- ].join("\n");
192
- stream.write(`${content}\n`);
179
+ ].join("\n")}\n`;
193
180
  }
194
181
  //#endregion
195
182
  //#region src/context/prompts.ts
@@ -270,7 +257,7 @@ function createMemoryStore() {
270
257
  /**
271
258
  * Create the {@link Context} object threaded through middleware and command handlers.
272
259
  *
273
- * Assembles logger, spinner, output, store, prompts, and meta from
260
+ * Assembles logger, spinner, format, store, prompts, and meta from
274
261
  * the provided options into a single immutable context. Each sub-system is
275
262
  * constructed via its own factory so this function remains a lean orchestrator.
276
263
  *
@@ -280,7 +267,7 @@ function createMemoryStore() {
280
267
  function createContext(options) {
281
268
  const ctxLogger = options.logger ?? createCliLogger();
282
269
  const ctxSpinner = clack.spinner();
283
- const ctxOutput = createContextOutput(options.output ?? process.stdout);
270
+ const ctxFormat = createContextFormat();
284
271
  const ctxStore = createMemoryStore();
285
272
  const ctxPrompts = createContextPrompts();
286
273
  const ctxMeta = {
@@ -290,13 +277,14 @@ function createContext(options) {
290
277
  };
291
278
  return {
292
279
  args: options.args,
280
+ colors: Object.freeze({ ...pc }),
293
281
  config: options.config,
294
282
  fail(message, failOptions) {
295
283
  throw createContextError(message, failOptions);
296
284
  },
285
+ format: ctxFormat,
297
286
  logger: ctxLogger,
298
287
  meta: ctxMeta,
299
- output: ctxOutput,
300
288
  prompts: ctxPrompts,
301
289
  spinner: ctxSpinner,
302
290
  store: ctxStore
@@ -318,17 +306,7 @@ const INDEX_NAME = "index";
318
306
  */
319
307
  async function autoload(options) {
320
308
  const dir = resolveDir(options);
321
- const entries = await readdir(dir, { withFileTypes: true });
322
- const fileEntries = entries.filter(isCommandFile);
323
- const dirEntries = entries.filter(isCommandDir);
324
- const fileResults = await Promise.all(fileEntries.map(async (entry) => {
325
- const cmd = await importCommand(join(dir, entry.name));
326
- if (!cmd) return;
327
- return [deriveCommandName(entry), cmd];
328
- }));
329
- const dirResults = await Promise.all(dirEntries.map((entry) => buildDirCommand(join(dir, entry.name))));
330
- const validPairs = [...fileResults, ...dirResults].filter((pair) => pair !== void 0);
331
- return Object.fromEntries(validPairs);
309
+ return buildCommandMapFromEntries(dir, await readdir(dir, { withFileTypes: true }));
332
310
  }
333
311
  /**
334
312
  * Resolve the target directory from autoload options.
@@ -355,7 +333,7 @@ function resolveDir(options) {
355
333
  async function buildDirCommand(dir) {
356
334
  const name = basename(dir);
357
335
  const dirEntries = await readdir(dir, { withFileTypes: true });
358
- const subCommands = await buildSubCommands(dir, dirEntries);
336
+ const subCommands = await buildCommandMapFromEntries(dir, dirEntries);
359
337
  const indexFile = findIndexInEntries(dirEntries);
360
338
  if (indexFile) {
361
339
  const parentCommand = await importCommand(join(dir, indexFile.name));
@@ -368,14 +346,17 @@ async function buildDirCommand(dir) {
368
346
  return [name, withTag({ commands: subCommands }, "Command")];
369
347
  }
370
348
  /**
371
- * Build subcommands from already-read directory entries, avoiding a redundant readdir call.
349
+ * Build a CommandMap from pre-read directory entries.
350
+ *
351
+ * Shared by both `autoload` and `buildDirCommand` to avoid duplicating
352
+ * the file/dir fan-out and result-filtering logic.
372
353
  *
373
354
  * @private
374
- * @param dir - Absolute path to the directory.
375
- * @param entries - Pre-read directory entries.
355
+ * @param dir - Absolute path to the directory the entries belong to.
356
+ * @param entries - Pre-read directory entries for that directory.
376
357
  * @returns A CommandMap built from the entries.
377
358
  */
378
- async function buildSubCommands(dir, entries) {
359
+ async function buildCommandMapFromEntries(dir, entries) {
379
360
  const fileEntries = entries.filter(isCommandFile);
380
361
  const dirEntries = entries.filter(isCommandDir);
381
362
  const fileResults = await Promise.all(fileEntries.map(async (entry) => {