@kidd-cli/core 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{config-BvGapuFJ.js → config-Db_sjFU-.js} +60 -65
- package/dist/config-Db_sjFU-.js.map +1 -0
- package/dist/create-http-client-tZJWlWp1.js +165 -0
- package/dist/create-http-client-tZJWlWp1.js.map +1 -0
- package/dist/{create-store-BQUX0tAn.js → create-store-D-fQpCql.js} +32 -4
- package/dist/create-store-D-fQpCql.js.map +1 -0
- package/dist/index.d.ts +21 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -17
- package/dist/index.js.map +1 -1
- package/dist/lib/config.js +2 -2
- package/dist/lib/project.d.ts +1 -1
- package/dist/lib/project.d.ts.map +1 -1
- package/dist/lib/project.js +1 -1
- package/dist/lib/store.d.ts +2 -1
- package/dist/lib/store.d.ts.map +1 -1
- package/dist/lib/store.js +2 -2
- package/dist/middleware/auth.d.ts +223 -14
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +973 -408
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/http.d.ts +10 -16
- package/dist/middleware/http.d.ts.map +1 -1
- package/dist/middleware/http.js +21 -221
- package/dist/middleware/http.js.map +1 -1
- package/dist/{middleware-D3psyhYo.js → middleware-BFBKNSPQ.js} +13 -2
- package/dist/{middleware-D3psyhYo.js.map → middleware-BFBKNSPQ.js.map} +1 -1
- package/dist/{project-NPtYX2ZX.js → project-DuXgjaa_.js} +19 -16
- package/dist/project-DuXgjaa_.js.map +1 -0
- package/dist/{types-kjpRau0U.d.ts → types-BaZ5WqVM.d.ts} +78 -13
- package/dist/types-BaZ5WqVM.d.ts.map +1 -0
- package/dist/{types-Cz9h927W.d.ts → types-C0CYivzY.d.ts} +1 -1
- package/dist/{types-Cz9h927W.d.ts.map → types-C0CYivzY.d.ts.map} +1 -1
- package/package.json +5 -12
- package/dist/config-BvGapuFJ.js.map +0 -1
- package/dist/create-store-BQUX0tAn.js.map +0 -1
- package/dist/lib/output.d.ts +0 -62
- package/dist/lib/output.d.ts.map +0 -1
- package/dist/lib/output.js +0 -276
- package/dist/lib/output.js.map +0 -1
- package/dist/lib/prompts.d.ts +0 -24
- package/dist/lib/prompts.d.ts.map +0 -1
- package/dist/lib/prompts.js +0 -3
- package/dist/project-NPtYX2ZX.js.map +0 -1
- package/dist/prompts-lLfUSgd6.js +0 -63
- package/dist/prompts-lLfUSgd6.js.map +0 -1
- package/dist/types-CqKJhsYk.d.ts +0 -135
- package/dist/types-CqKJhsYk.d.ts.map +0 -1
- package/dist/types-DFtYg5uZ.d.ts +0 -26
- package/dist/types-DFtYg5uZ.d.ts.map +0 -1
- package/dist/types-kjpRau0U.d.ts.map +0 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { i as findProjectRoot } from "./project-
|
|
1
|
+
import { i as findProjectRoot } from "./project-DuXgjaa_.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";
|
|
5
|
-
import {
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
6
|
import { formatZodIssues } from "@kidd-cli/utils/validate";
|
|
7
|
+
import { fileExists } from "@kidd-cli/utils/fs";
|
|
7
8
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
8
9
|
import { parse as parse$1, stringify } from "yaml";
|
|
9
10
|
|
|
@@ -37,33 +38,6 @@ function getConfigFileNames(name) {
|
|
|
37
38
|
return CONFIG_EXTENSIONS.map((ext) => `.${name}${ext}`);
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
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
41
|
* Search for a config file across multiple directories.
|
|
68
42
|
*
|
|
69
43
|
* Searches in order: explicit search paths, the current working directory,
|
|
@@ -88,6 +62,24 @@ async function findConfig(options) {
|
|
|
88
62
|
}
|
|
89
63
|
return null;
|
|
90
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Search a single directory for the first matching config file name.
|
|
67
|
+
*
|
|
68
|
+
* Checks each candidate file name in order and returns the path of the first
|
|
69
|
+
* one that exists on disk.
|
|
70
|
+
*
|
|
71
|
+
* @param dir - The directory to search in.
|
|
72
|
+
* @param fileNames - Candidate config file names to look for.
|
|
73
|
+
* @returns The full path to the first matching config file, or null if none found.
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
async function findConfigFile(dir, fileNames) {
|
|
77
|
+
return (await Promise.all(fileNames.map(async (fileName) => {
|
|
78
|
+
const filePath = join(dir, fileName);
|
|
79
|
+
if (await fileExists(filePath)) return filePath;
|
|
80
|
+
return null;
|
|
81
|
+
}))).find((result) => result !== null) ?? null;
|
|
82
|
+
}
|
|
91
83
|
|
|
92
84
|
//#endregion
|
|
93
85
|
//#region src/lib/config/parse.ts
|
|
@@ -101,11 +93,47 @@ function getFormat(filePath) {
|
|
|
101
93
|
return match(extname(filePath)).with(".jsonc", () => "jsonc").with(".yaml", () => "yaml").otherwise(() => "json");
|
|
102
94
|
}
|
|
103
95
|
/**
|
|
96
|
+
* Parse config file content using the appropriate parser for the given format.
|
|
97
|
+
*
|
|
98
|
+
* @param options - Parse content options.
|
|
99
|
+
* @returns A ConfigOperationResult with the parsed data or an error.
|
|
100
|
+
*/
|
|
101
|
+
function parseContent(options) {
|
|
102
|
+
const { content, filePath, format } = options;
|
|
103
|
+
return match(format).with("json", () => parseJson(content, filePath)).with("jsonc", () => parseJsoncContent(content, filePath)).with("yaml", () => parseYamlContent(content, filePath)).exhaustive();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Serialize data to a string in the specified config format.
|
|
107
|
+
*
|
|
108
|
+
* @param data - The data to serialize.
|
|
109
|
+
* @param format - The target config format.
|
|
110
|
+
* @returns The serialized string representation.
|
|
111
|
+
*/
|
|
112
|
+
function serializeContent(data, format) {
|
|
113
|
+
return match(format).with("json", () => {
|
|
114
|
+
const [, json] = jsonStringify(data, { pretty: true });
|
|
115
|
+
return `${json}\n`;
|
|
116
|
+
}).with("jsonc", () => {
|
|
117
|
+
const [, json] = jsonStringify(data, { pretty: true });
|
|
118
|
+
return `${json}\n`;
|
|
119
|
+
}).with("yaml", () => stringify(data)).exhaustive();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get the file extension string for a given config format.
|
|
123
|
+
*
|
|
124
|
+
* @param format - The config format.
|
|
125
|
+
* @returns The file extension including the leading dot (e.g. '.json').
|
|
126
|
+
*/
|
|
127
|
+
function getExtension(format) {
|
|
128
|
+
return match(format).with("json", () => ".json").with("jsonc", () => ".jsonc").with("yaml", () => ".yaml").exhaustive();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
104
131
|
* Parse a JSON string and return the result as a ConfigOperationResult.
|
|
105
132
|
*
|
|
106
133
|
* @param content - The raw JSON string to parse.
|
|
107
134
|
* @param filePath - The file path used in error messages.
|
|
108
135
|
* @returns A ConfigOperationResult with the parsed data or a parse error.
|
|
136
|
+
* @private
|
|
109
137
|
*/
|
|
110
138
|
function parseJson(content, filePath) {
|
|
111
139
|
const [error, result] = jsonParse(content);
|
|
@@ -118,6 +146,7 @@ function parseJson(content, filePath) {
|
|
|
118
146
|
* @param content - The raw JSONC string to parse.
|
|
119
147
|
* @param filePath - The file path used in error messages.
|
|
120
148
|
* @returns A ConfigOperationResult with the parsed data or a parse error.
|
|
149
|
+
* @private
|
|
121
150
|
*/
|
|
122
151
|
function parseJsoncContent(content, filePath) {
|
|
123
152
|
const errors = [];
|
|
@@ -134,47 +163,13 @@ function parseJsoncContent(content, filePath) {
|
|
|
134
163
|
* @param content - The raw YAML string to parse.
|
|
135
164
|
* @param filePath - The file path used in error messages.
|
|
136
165
|
* @returns A ConfigOperationResult with the parsed data or a parse error.
|
|
166
|
+
* @private
|
|
137
167
|
*/
|
|
138
168
|
function parseYamlContent(content, filePath) {
|
|
139
169
|
const [error, result] = attempt(() => parse$1(content));
|
|
140
170
|
if (error) return err(`Failed to parse YAML in ${filePath}: ${String(error)}`);
|
|
141
171
|
return [null, result];
|
|
142
172
|
}
|
|
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
173
|
|
|
179
174
|
//#endregion
|
|
180
175
|
//#region src/lib/config/create-config.ts
|
|
@@ -279,4 +274,4 @@ function resolveReadErrorDetail(readError) {
|
|
|
279
274
|
|
|
280
275
|
//#endregion
|
|
281
276
|
export { DEFAULT_EXIT_CODE as n, createConfigClient as t };
|
|
282
|
-
//# sourceMappingURL=config-
|
|
277
|
+
//# sourceMappingURL=config-Db_sjFU-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-Db_sjFU-.js","names":["stringifyYaml","parseJsonc","parseYaml"],"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 { 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(\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","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(\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 * @private\n */\nfunction 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","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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"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { attemptAsync } from "@kidd-cli/utils/fp";
|
|
2
|
+
|
|
3
|
+
//#region src/middleware/http/create-http-client.ts
|
|
4
|
+
/**
|
|
5
|
+
* Typed HTTP client factory.
|
|
6
|
+
*
|
|
7
|
+
* Creates a closure-based {@link HttpClient} with pre-configured base URL
|
|
8
|
+
* and default headers. All methods delegate to a shared request executor.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Create a typed HTTP client with pre-configured base URL and headers.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Client configuration.
|
|
16
|
+
* @returns An HttpClient instance.
|
|
17
|
+
*/
|
|
18
|
+
function createHttpClient(options) {
|
|
19
|
+
const { baseUrl, defaultHeaders, resolveHeaders } = options;
|
|
20
|
+
return {
|
|
21
|
+
delete: (path, requestOptions) => executeRequest(baseUrl, "DELETE", path, defaultHeaders, resolveHeaders, requestOptions),
|
|
22
|
+
get: (path, requestOptions) => executeRequest(baseUrl, "GET", path, defaultHeaders, resolveHeaders, requestOptions),
|
|
23
|
+
patch: (path, requestOptions) => executeRequest(baseUrl, "PATCH", path, defaultHeaders, resolveHeaders, requestOptions),
|
|
24
|
+
post: (path, requestOptions) => executeRequest(baseUrl, "POST", path, defaultHeaders, resolveHeaders, requestOptions),
|
|
25
|
+
put: (path, requestOptions) => executeRequest(baseUrl, "PUT", path, defaultHeaders, resolveHeaders, requestOptions)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the full URL from base, path, and optional query params.
|
|
30
|
+
*
|
|
31
|
+
* @private
|
|
32
|
+
* @param baseUrl - The base URL.
|
|
33
|
+
* @param path - The request path.
|
|
34
|
+
* @param params - Optional query parameters.
|
|
35
|
+
* @returns The fully qualified URL string.
|
|
36
|
+
*/
|
|
37
|
+
function buildUrl(baseUrl, path, params) {
|
|
38
|
+
const url = new URL(path, baseUrl);
|
|
39
|
+
if (params !== void 0) url.search = new URLSearchParams(params).toString();
|
|
40
|
+
return url.toString();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Merge default, dynamic, and per-request headers into a single record.
|
|
44
|
+
*
|
|
45
|
+
* Per-request headers take highest priority, then dynamic headers,
|
|
46
|
+
* then default headers.
|
|
47
|
+
*
|
|
48
|
+
* @private
|
|
49
|
+
* @param defaultHeaders - Optional default headers.
|
|
50
|
+
* @param dynamicHeaders - Optional dynamically resolved headers.
|
|
51
|
+
* @param requestHeaders - Optional per-request headers.
|
|
52
|
+
* @returns The merged headers record.
|
|
53
|
+
*/
|
|
54
|
+
function mergeHeaders(defaultHeaders, dynamicHeaders, requestHeaders) {
|
|
55
|
+
return {
|
|
56
|
+
...defaultHeaders,
|
|
57
|
+
...dynamicHeaders,
|
|
58
|
+
...requestHeaders
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Normalize optional request options into a concrete object with safe defaults.
|
|
63
|
+
*
|
|
64
|
+
* When `options` is `undefined`, returns an empty object so callers can use
|
|
65
|
+
* direct property access without additional nil checks.
|
|
66
|
+
*
|
|
67
|
+
* @private
|
|
68
|
+
* @param options - Optional per-request options.
|
|
69
|
+
* @returns The resolved options object.
|
|
70
|
+
*/
|
|
71
|
+
function resolveRequestOptions(options) {
|
|
72
|
+
if (options !== void 0) return options;
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Invoke the dynamic header resolver if provided.
|
|
77
|
+
*
|
|
78
|
+
* @private
|
|
79
|
+
* @param resolveHeaders - Optional function to resolve dynamic headers.
|
|
80
|
+
* @returns The resolved headers record, or undefined.
|
|
81
|
+
*/
|
|
82
|
+
function resolveDynamicHeaders(resolveHeaders) {
|
|
83
|
+
if (resolveHeaders !== void 0) return resolveHeaders();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Resolve the serialized body string and content-type header mutation.
|
|
87
|
+
*
|
|
88
|
+
* @private
|
|
89
|
+
* @param options - Optional per-request options.
|
|
90
|
+
* @returns The serialized body string or undefined.
|
|
91
|
+
*/
|
|
92
|
+
function resolveBody(options) {
|
|
93
|
+
if (options !== void 0 && options.body !== void 0) return JSON.stringify(options.body);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build the fetch init options from resolved values.
|
|
97
|
+
*
|
|
98
|
+
* @private
|
|
99
|
+
* @param method - The HTTP method.
|
|
100
|
+
* @param headers - The merged headers.
|
|
101
|
+
* @param body - The serialized body or undefined.
|
|
102
|
+
* @param signal - The abort signal or undefined.
|
|
103
|
+
* @returns The RequestInit for fetch.
|
|
104
|
+
*/
|
|
105
|
+
function buildFetchInit(method, headers, body, signal) {
|
|
106
|
+
if (body !== void 0) return {
|
|
107
|
+
body,
|
|
108
|
+
headers: {
|
|
109
|
+
...headers,
|
|
110
|
+
"Content-Type": "application/json"
|
|
111
|
+
},
|
|
112
|
+
method,
|
|
113
|
+
signal
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
headers,
|
|
117
|
+
method,
|
|
118
|
+
signal
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Execute an HTTP request and wrap the response.
|
|
123
|
+
*
|
|
124
|
+
* @private
|
|
125
|
+
* @param baseUrl - The base URL.
|
|
126
|
+
* @param method - The HTTP method.
|
|
127
|
+
* @param path - The request path.
|
|
128
|
+
* @param defaultHeaders - Optional default headers.
|
|
129
|
+
* @param resolveHeaders - Optional function to resolve dynamic headers per-request.
|
|
130
|
+
* @param options - Optional per-request options.
|
|
131
|
+
* @returns A typed response wrapper.
|
|
132
|
+
*/
|
|
133
|
+
async function executeRequest(baseUrl, method, path, defaultHeaders, resolveHeaders, options) {
|
|
134
|
+
const resolved = resolveRequestOptions(options);
|
|
135
|
+
const url = buildUrl(baseUrl, path, resolved.params);
|
|
136
|
+
const init = buildFetchInit(method, mergeHeaders(defaultHeaders, resolveDynamicHeaders(resolveHeaders), resolved.headers), resolveBody(options), resolved.signal);
|
|
137
|
+
const response = await fetch(url, init);
|
|
138
|
+
return {
|
|
139
|
+
data: await parseResponseBody(response),
|
|
140
|
+
headers: response.headers,
|
|
141
|
+
ok: response.ok,
|
|
142
|
+
raw: response,
|
|
143
|
+
status: response.status
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Parse the response body as JSON, returning null on failure.
|
|
148
|
+
*
|
|
149
|
+
* Wraps `response.json()` with `attemptAsync` so malformed API
|
|
150
|
+
* responses do not crash the command. Returns `null as TResponse`
|
|
151
|
+
* when parsing fails.
|
|
152
|
+
*
|
|
153
|
+
* @private
|
|
154
|
+
* @param response - The fetch Response.
|
|
155
|
+
* @returns The parsed body or null.
|
|
156
|
+
*/
|
|
157
|
+
async function parseResponseBody(response) {
|
|
158
|
+
const [error, data] = await attemptAsync(() => response.json());
|
|
159
|
+
if (error) return null;
|
|
160
|
+
return data;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
//#endregion
|
|
164
|
+
export { createHttpClient as t };
|
|
165
|
+
//# sourceMappingURL=create-http-client-tZJWlWp1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-http-client-tZJWlWp1.js","names":[],"sources":["../src/middleware/http/create-http-client.ts"],"sourcesContent":["/**\n * Typed HTTP client factory.\n *\n * Creates a closure-based {@link HttpClient} with pre-configured base URL\n * and default headers. All methods delegate to a shared request executor.\n *\n * @module\n */\n\nimport { attemptAsync } from '@kidd-cli/utils/fp'\n\nimport type { HttpClient, RequestOptions, TypedResponse } from './types.js'\n\n/**\n * Options for creating an HTTP client.\n */\ninterface CreateHttpClientOptions {\n readonly baseUrl: string\n readonly defaultHeaders?: Readonly<Record<string, string>>\n readonly resolveHeaders?: () => Readonly<Record<string, string>>\n}\n\n/**\n * Create a typed HTTP client with pre-configured base URL and headers.\n *\n * @param options - Client configuration.\n * @returns An HttpClient instance.\n */\nexport function createHttpClient(options: CreateHttpClientOptions): HttpClient {\n const { baseUrl, defaultHeaders, resolveHeaders } = options\n\n return {\n delete: <TResponse = unknown>(path: string, requestOptions?: RequestOptions) =>\n executeRequest<TResponse>(baseUrl, 'DELETE', path, defaultHeaders, resolveHeaders, requestOptions),\n\n get: <TResponse = unknown>(path: string, requestOptions?: RequestOptions) =>\n executeRequest<TResponse>(baseUrl, 'GET', path, defaultHeaders, resolveHeaders, requestOptions),\n\n patch: <TResponse = unknown, TBody = unknown>(\n path: string,\n requestOptions?: RequestOptions<TBody>\n ) =>\n executeRequest<TResponse>(baseUrl, 'PATCH', path, defaultHeaders, resolveHeaders, requestOptions),\n\n post: <TResponse = unknown, TBody = unknown>(\n path: string,\n requestOptions?: RequestOptions<TBody>\n ) =>\n executeRequest<TResponse>(baseUrl, 'POST', path, defaultHeaders, resolveHeaders, requestOptions),\n\n put: <TResponse = unknown, TBody = unknown>(\n path: string,\n requestOptions?: RequestOptions<TBody>\n ) =>\n executeRequest<TResponse>(baseUrl, 'PUT', path, defaultHeaders, resolveHeaders, requestOptions),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build the full URL from base, path, and optional query params.\n *\n * @private\n * @param baseUrl - The base URL.\n * @param path - The request path.\n * @param params - Optional query parameters.\n * @returns The fully qualified URL string.\n */\nfunction buildUrl(\n baseUrl: string,\n path: string,\n params: Readonly<Record<string, string>> | undefined\n): string {\n const url = new URL(path, baseUrl)\n\n if (params !== undefined) {\n const searchParams = new URLSearchParams(params)\n url.search = searchParams.toString()\n }\n\n return url.toString()\n}\n\n/**\n * Merge default, dynamic, and per-request headers into a single record.\n *\n * Per-request headers take highest priority, then dynamic headers,\n * then default headers.\n *\n * @private\n * @param defaultHeaders - Optional default headers.\n * @param dynamicHeaders - Optional dynamically resolved headers.\n * @param requestHeaders - Optional per-request headers.\n * @returns The merged headers record.\n */\nfunction mergeHeaders(\n defaultHeaders: Readonly<Record<string, string>> | undefined,\n dynamicHeaders: Readonly<Record<string, string>> | undefined,\n requestHeaders: Readonly<Record<string, string>> | undefined\n): Readonly<Record<string, string>> {\n return {\n ...defaultHeaders,\n ...dynamicHeaders,\n ...requestHeaders,\n }\n}\n\n/**\n * Normalize optional request options into a concrete object with safe defaults.\n *\n * When `options` is `undefined`, returns an empty object so callers can use\n * direct property access without additional nil checks.\n *\n * @private\n * @param options - Optional per-request options.\n * @returns The resolved options object.\n */\nfunction resolveRequestOptions(options: RequestOptions | undefined): RequestOptions {\n if (options !== undefined) {\n return options\n }\n\n return {}\n}\n\n/**\n * Invoke the dynamic header resolver if provided.\n *\n * @private\n * @param resolveHeaders - Optional function to resolve dynamic headers.\n * @returns The resolved headers record, or undefined.\n */\nfunction resolveDynamicHeaders(\n resolveHeaders: (() => Readonly<Record<string, string>>) | undefined\n): Readonly<Record<string, string>> | undefined {\n if (resolveHeaders !== undefined) {\n return resolveHeaders()\n }\n\n return undefined\n}\n\n/**\n * Resolve the serialized body string and content-type header mutation.\n *\n * @private\n * @param options - Optional per-request options.\n * @returns The serialized body string or undefined.\n */\nfunction resolveBody(options: RequestOptions | undefined): string | undefined {\n if (options !== undefined && options.body !== undefined) {\n return JSON.stringify(options.body)\n }\n\n return undefined\n}\n\n/**\n * Build the fetch init options from resolved values.\n *\n * @private\n * @param method - The HTTP method.\n * @param headers - The merged headers.\n * @param body - The serialized body or undefined.\n * @param signal - The abort signal or undefined.\n * @returns The RequestInit for fetch.\n */\nfunction buildFetchInit(\n method: string,\n headers: Readonly<Record<string, string>>,\n body: string | undefined,\n signal: AbortSignal | undefined\n): RequestInit {\n if (body !== undefined) {\n return {\n body,\n headers: { ...headers, 'Content-Type': 'application/json' },\n method,\n signal,\n }\n }\n\n return {\n headers,\n method,\n signal,\n }\n}\n\n/**\n * Execute an HTTP request and wrap the response.\n *\n * @private\n * @param baseUrl - The base URL.\n * @param method - The HTTP method.\n * @param path - The request path.\n * @param defaultHeaders - Optional default headers.\n * @param resolveHeaders - Optional function to resolve dynamic headers per-request.\n * @param options - Optional per-request options.\n * @returns A typed response wrapper.\n */\nasync function executeRequest<TResponse>(\n baseUrl: string,\n method: string,\n path: string,\n defaultHeaders: Readonly<Record<string, string>> | undefined,\n resolveHeaders: (() => Readonly<Record<string, string>>) | undefined,\n options: RequestOptions | undefined\n): Promise<TypedResponse<TResponse>> {\n const resolved = resolveRequestOptions(options)\n const url = buildUrl(baseUrl, path, resolved.params)\n const dynamicHeaders = resolveDynamicHeaders(resolveHeaders)\n const headers = mergeHeaders(defaultHeaders, dynamicHeaders, resolved.headers)\n const body = resolveBody(options)\n const init = buildFetchInit(method, headers, body, resolved.signal)\n\n const response = await fetch(url, init)\n const data = await parseResponseBody<TResponse>(response)\n\n return {\n data,\n headers: response.headers,\n ok: response.ok,\n raw: response,\n status: response.status,\n }\n}\n\n/**\n * Parse the response body as JSON, returning null on failure.\n *\n * Wraps `response.json()` with `attemptAsync` so malformed API\n * responses do not crash the command. Returns `null as TResponse`\n * when parsing fails.\n *\n * @private\n * @param response - The fetch Response.\n * @returns The parsed body or null.\n */\nasync function parseResponseBody<TResponse>(response: Response): Promise<TResponse> {\n const [error, data] = await attemptAsync(() => response.json() as Promise<TResponse>)\n\n if (error) {\n return null as TResponse\n }\n\n return data as TResponse\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA4BA,SAAgB,iBAAiB,SAA8C;CAC7E,MAAM,EAAE,SAAS,gBAAgB,mBAAmB;AAEpD,QAAO;EACL,SAA8B,MAAc,mBAC1C,eAA0B,SAAS,UAAU,MAAM,gBAAgB,gBAAgB,eAAe;EAEpG,MAA2B,MAAc,mBACvC,eAA0B,SAAS,OAAO,MAAM,gBAAgB,gBAAgB,eAAe;EAEjG,QACE,MACA,mBAEA,eAA0B,SAAS,SAAS,MAAM,gBAAgB,gBAAgB,eAAe;EAEnG,OACE,MACA,mBAEA,eAA0B,SAAS,QAAQ,MAAM,gBAAgB,gBAAgB,eAAe;EAElG,MACE,MACA,mBAEA,eAA0B,SAAS,OAAO,MAAM,gBAAgB,gBAAgB,eAAe;EAClG;;;;;;;;;;;AAgBH,SAAS,SACP,SACA,MACA,QACQ;CACR,MAAM,MAAM,IAAI,IAAI,MAAM,QAAQ;AAElC,KAAI,WAAW,OAEb,KAAI,SADiB,IAAI,gBAAgB,OAAO,CACtB,UAAU;AAGtC,QAAO,IAAI,UAAU;;;;;;;;;;;;;;AAevB,SAAS,aACP,gBACA,gBACA,gBACkC;AAClC,QAAO;EACL,GAAG;EACH,GAAG;EACH,GAAG;EACJ;;;;;;;;;;;;AAaH,SAAS,sBAAsB,SAAqD;AAClF,KAAI,YAAY,OACd,QAAO;AAGT,QAAO,EAAE;;;;;;;;;AAUX,SAAS,sBACP,gBAC8C;AAC9C,KAAI,mBAAmB,OACrB,QAAO,gBAAgB;;;;;;;;;AAa3B,SAAS,YAAY,SAAyD;AAC5E,KAAI,YAAY,UAAa,QAAQ,SAAS,OAC5C,QAAO,KAAK,UAAU,QAAQ,KAAK;;;;;;;;;;;;AAgBvC,SAAS,eACP,QACA,SACA,MACA,QACa;AACb,KAAI,SAAS,OACX,QAAO;EACL;EACA,SAAS;GAAE,GAAG;GAAS,gBAAgB;GAAoB;EAC3D;EACA;EACD;AAGH,QAAO;EACL;EACA;EACA;EACD;;;;;;;;;;;;;;AAeH,eAAe,eACb,SACA,QACA,MACA,gBACA,gBACA,SACmC;CACnC,MAAM,WAAW,sBAAsB,QAAQ;CAC/C,MAAM,MAAM,SAAS,SAAS,MAAM,SAAS,OAAO;CAIpD,MAAM,OAAO,eAAe,QAFZ,aAAa,gBADN,sBAAsB,eAAe,EACC,SAAS,QAAQ,EACjE,YAAY,QAAQ,EACkB,SAAS,OAAO;CAEnE,MAAM,WAAW,MAAM,MAAM,KAAK,KAAK;AAGvC,QAAO;EACL,MAHW,MAAM,kBAA6B,SAAS;EAIvD,SAAS,SAAS;EAClB,IAAI,SAAS;EACb,KAAK;EACL,QAAQ,SAAS;EAClB;;;;;;;;;;;;;AAcH,eAAe,kBAA6B,UAAwC;CAClF,MAAM,CAAC,OAAO,QAAQ,MAAM,mBAAmB,SAAS,MAAM,CAAuB;AAErF,KAAI,MACF,QAAO;AAGT,QAAO"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { n as resolveLocalPath, t as resolveGlobalPath } from "./project-
|
|
1
|
+
import { n as resolveLocalPath, t as resolveGlobalPath } from "./project-DuXgjaa_.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";
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
|
|
7
7
|
//#region src/lib/store/create-store.ts
|
|
8
8
|
/**
|
|
@@ -44,7 +44,6 @@ function createStore(options) {
|
|
|
44
44
|
* @returns The file content, or null if the file does not exist or cannot be read.
|
|
45
45
|
*/
|
|
46
46
|
function loadFromPath(filePath) {
|
|
47
|
-
if (!existsSync(filePath)) return null;
|
|
48
47
|
const [error, content] = attempt(() => readFileSync(filePath, "utf8"));
|
|
49
48
|
if (error) return null;
|
|
50
49
|
return content;
|
|
@@ -172,12 +171,41 @@ function createStore(options) {
|
|
|
172
171
|
if (writeError) return err(writeError);
|
|
173
172
|
return ok(filePath);
|
|
174
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Remove a file from the store.
|
|
176
|
+
*
|
|
177
|
+
* Returns `ok(filePath)` when the file was deleted or did not exist
|
|
178
|
+
* (idempotent). Returns an error when the target directory cannot be
|
|
179
|
+
* resolved or the unlink fails.
|
|
180
|
+
*
|
|
181
|
+
* @private
|
|
182
|
+
* @param filename - The filename to remove.
|
|
183
|
+
* @param removeOptions - Options controlling the removal target.
|
|
184
|
+
* @returns A Result with the file path on success.
|
|
185
|
+
*/
|
|
186
|
+
function remove(filename, removeOptions = {}) {
|
|
187
|
+
const { source: removeSource = "global", startDir } = removeOptions;
|
|
188
|
+
const dir = resolveSaveDir({
|
|
189
|
+
globalDir: getGlobalDir(),
|
|
190
|
+
localDir: getLocalDir(startDir),
|
|
191
|
+
source: removeSource
|
|
192
|
+
});
|
|
193
|
+
if (dir === null) return err(/* @__PURE__ */ new Error(`Cannot remove from "${removeSource}" — no local project directory found`));
|
|
194
|
+
const filePath = join(dir, filename);
|
|
195
|
+
if (!existsSync(filePath)) return ok(filePath);
|
|
196
|
+
const [removeError] = attempt(() => {
|
|
197
|
+
unlinkSync(filePath);
|
|
198
|
+
});
|
|
199
|
+
if (removeError) return err(removeError);
|
|
200
|
+
return ok(filePath);
|
|
201
|
+
}
|
|
175
202
|
return {
|
|
176
203
|
getFilePath,
|
|
177
204
|
getGlobalDir,
|
|
178
205
|
getLocalDir,
|
|
179
206
|
load,
|
|
180
207
|
loadRaw,
|
|
208
|
+
remove,
|
|
181
209
|
save
|
|
182
210
|
};
|
|
183
211
|
}
|
|
@@ -194,4 +222,4 @@ function resolveSaveDir(options) {
|
|
|
194
222
|
|
|
195
223
|
//#endregion
|
|
196
224
|
export { createStore as t };
|
|
197
|
-
//# sourceMappingURL=create-store-
|
|
225
|
+
//# sourceMappingURL=create-store-D-fQpCql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-store-D-fQpCql.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(new Error(`Cannot remove from \"${removeSource}\" — no local project directory found`))\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,4 +1,4 @@
|
|
|
1
|
-
import { a as CommandDef, c as
|
|
1
|
+
import { a as CommandDef, c as MiddlewareEnv, i as Command, l as MiddlewareFn, n as AutoloadOptions, o as CommandMap, r as CliOptions, s as Middleware, t as ArgsDef, u as Context } from "./types-BaZ5WqVM.js";
|
|
2
2
|
import { defineConfig } from "@kidd-cli/config";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
@@ -17,10 +17,14 @@ declare function cli<TSchema extends z.ZodType = z.ZodType>(options: CliOptions<
|
|
|
17
17
|
/**
|
|
18
18
|
* Define a CLI command with typed args, config, and handler.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
20
|
+
* The `const TMiddleware` generic preserves the middleware tuple as a literal type,
|
|
21
|
+
* enabling TypeScript to extract and intersect `Variables` from each middleware
|
|
22
|
+
* element onto the handler's `ctx` type.
|
|
23
|
+
*
|
|
24
|
+
* @param def - Command definition including description, args schema, middleware, and handler.
|
|
21
25
|
* @returns A resolved Command object for registration in the command map.
|
|
22
26
|
*/
|
|
23
|
-
declare function command<TArgsDef extends ArgsDef = ArgsDef, TConfig extends Record<string, unknown> = Record<string, unknown
|
|
27
|
+
declare function command<TArgsDef extends ArgsDef = ArgsDef, TConfig extends Record<string, unknown> = Record<string, unknown>, const TMiddleware extends readonly Middleware<MiddlewareEnv>[] = readonly Middleware<MiddlewareEnv>[]>(def: CommandDef<TArgsDef, TConfig, TMiddleware>): Command;
|
|
24
28
|
//#endregion
|
|
25
29
|
//#region src/autoloader.d.ts
|
|
26
30
|
/**
|
|
@@ -64,10 +68,21 @@ declare function decorateContext<TKey extends string, TValue>(ctx: Context, key:
|
|
|
64
68
|
/**
|
|
65
69
|
* Create a typed middleware that runs before command handlers.
|
|
66
70
|
*
|
|
71
|
+
* Use the generic parameter to declare context variables the middleware provides.
|
|
72
|
+
* The handler's `ctx` type in downstream commands will include these variables.
|
|
73
|
+
*
|
|
67
74
|
* @param handler - The middleware function receiving ctx and next.
|
|
68
|
-
* @returns A Middleware object for use in the cli() middleware stack.
|
|
75
|
+
* @returns A Middleware object for use in the cli() or command() middleware stack.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const loadUser = middleware<{ Variables: { user: User } }>(async (ctx, next) => {
|
|
80
|
+
* decorateContext(ctx, 'user', await fetchUser())
|
|
81
|
+
* await next()
|
|
82
|
+
* })
|
|
83
|
+
* ```
|
|
69
84
|
*/
|
|
70
|
-
declare function middleware<
|
|
85
|
+
declare function middleware<TEnv extends MiddlewareEnv = MiddlewareEnv>(handler: MiddlewareFn<TEnv>): Middleware<TEnv>;
|
|
71
86
|
//#endregion
|
|
72
|
-
export { type Command, type Context, autoload, cli, command, decorateContext, defineConfig, middleware };
|
|
87
|
+
export { type Command, type Context, type MiddlewareEnv, autoload, cli, command, decorateContext, defineConfig, middleware };
|
|
73
88
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/cli.ts","../src/command.ts","../src/autoloader.ts","../src/context/decorate.ts","../src/middleware.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/cli.ts","../src/command.ts","../src/autoloader.ts","../src/context/decorate.ts","../src/middleware.ts"],"mappings":";;;;;;;;AAyBA;;;;;iBAAsB,GAAA,iBAAoB,CAAA,CAAE,OAAA,GAAU,CAAA,CAAE,OAAA,CAAA,CACtD,OAAA,EAAS,UAAA,CAAW,OAAA,IACnB,OAAA;;;;;;;AAFH;;;;;;iBCXgB,OAAA,kBACG,OAAA,GAAU,OAAA,kBACX,MAAA,oBAA0B,MAAA,sDACP,UAAA,CAAW,aAAA,eAA4B,UAAA,CAAW,aAAA,IAAA,CACrF,GAAA,EAAK,UAAA,CAAW,QAAA,EAAU,OAAA,EAAS,WAAA,IAAe,OAAA;;;;;;;ADOpD;;iBEPsB,QAAA,CAAS,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,UAAA;;;;;;;AFOnE;;;;;;;;;;;;;;;;;;;;;;iBGGgB,eAAA,6BAAA,CACd,GAAA,EAAK,OAAA,EACL,GAAA,EAAK,IAAA,EACL,KAAA,EAAO,MAAA,GACN,OAAA;;;;;;;AHPH;;;;;;;;;;;;;iBIJgB,UAAA,cAAwB,aAAA,GAAgB,aAAA,CAAA,CACtD,OAAA,EAAS,YAAA,CAAa,IAAA,IACrB,UAAA,CAAW,IAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createCliLogger } from "./lib/logger.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as
|
|
4
|
-
import
|
|
5
|
-
import "./project-NPtYX2ZX.js";
|
|
2
|
+
import { n as DEFAULT_EXIT_CODE, t as createConfigClient } from "./config-Db_sjFU-.js";
|
|
3
|
+
import { n as decorateContext, t as middleware } from "./middleware-BFBKNSPQ.js";
|
|
4
|
+
import "./project-DuXgjaa_.js";
|
|
6
5
|
import { basename, extname, join, resolve } from "node:path";
|
|
7
6
|
import { loadConfig } from "@kidd-cli/config/loader";
|
|
8
7
|
import { attemptAsync, err, isPlainObject, isString, ok } from "@kidd-cli/utils/fp";
|
|
9
8
|
import yargs from "yargs";
|
|
9
|
+
import * as clack from "@clack/prompts";
|
|
10
10
|
import { TAG, hasTag, withTag } from "@kidd-cli/utils/tag";
|
|
11
11
|
import { jsonStringify } from "@kidd-cli/utils/json";
|
|
12
12
|
import { readdir } from "node:fs/promises";
|
|
@@ -198,26 +198,24 @@ function writeTableToStream(stream, rows, keys) {
|
|
|
198
198
|
/**
|
|
199
199
|
* Create the interactive prompt methods for a context.
|
|
200
200
|
*
|
|
201
|
-
* @private
|
|
202
201
|
* @returns A Prompts instance backed by clack.
|
|
203
202
|
*/
|
|
204
203
|
function createContextPrompts() {
|
|
205
|
-
const utils = createPromptUtils();
|
|
206
204
|
return {
|
|
207
205
|
async confirm(opts) {
|
|
208
|
-
return unwrapCancelSignal(
|
|
206
|
+
return unwrapCancelSignal(await clack.confirm(opts));
|
|
209
207
|
},
|
|
210
208
|
async multiselect(opts) {
|
|
211
|
-
return unwrapCancelSignal(
|
|
209
|
+
return unwrapCancelSignal(await clack.multiselect(opts));
|
|
212
210
|
},
|
|
213
211
|
async password(opts) {
|
|
214
|
-
return unwrapCancelSignal(
|
|
212
|
+
return unwrapCancelSignal(await clack.password(opts));
|
|
215
213
|
},
|
|
216
214
|
async select(opts) {
|
|
217
|
-
return unwrapCancelSignal(
|
|
215
|
+
return unwrapCancelSignal(await clack.select(opts));
|
|
218
216
|
},
|
|
219
217
|
async text(opts) {
|
|
220
|
-
return unwrapCancelSignal(
|
|
218
|
+
return unwrapCancelSignal(await clack.text(opts));
|
|
221
219
|
}
|
|
222
220
|
};
|
|
223
221
|
}
|
|
@@ -228,13 +226,12 @@ function createContextPrompts() {
|
|
|
228
226
|
* the typed result value.
|
|
229
227
|
*
|
|
230
228
|
* @private
|
|
231
|
-
* @param utils - The prompt utils instance (for isCancel and cancel).
|
|
232
229
|
* @param result - The raw prompt result (value or cancel symbol).
|
|
233
230
|
* @returns The unwrapped typed value.
|
|
234
231
|
*/
|
|
235
|
-
function unwrapCancelSignal(
|
|
236
|
-
if (
|
|
237
|
-
|
|
232
|
+
function unwrapCancelSignal(result) {
|
|
233
|
+
if (clack.isCancel(result)) {
|
|
234
|
+
clack.cancel("Operation cancelled.");
|
|
238
235
|
throw createContextError("Prompt cancelled by user", {
|
|
239
236
|
code: "PROMPT_CANCELLED",
|
|
240
237
|
exitCode: DEFAULT_EXIT_CODE
|
|
@@ -286,7 +283,7 @@ function createMemoryStore() {
|
|
|
286
283
|
*/
|
|
287
284
|
function createContext(options) {
|
|
288
285
|
const ctxLogger = options.logger ?? createCliLogger();
|
|
289
|
-
const ctxSpinner =
|
|
286
|
+
const ctxSpinner = clack.spinner();
|
|
290
287
|
const ctxOutput = createContextOutput(options.output ?? process.stdout);
|
|
291
288
|
const ctxStore = createMemoryStore();
|
|
292
289
|
const ctxPrompts = createContextPrompts();
|
|
@@ -1022,7 +1019,11 @@ function exitOnError(error, logger) {
|
|
|
1022
1019
|
/**
|
|
1023
1020
|
* Define a CLI command with typed args, config, and handler.
|
|
1024
1021
|
*
|
|
1025
|
-
*
|
|
1022
|
+
* The `const TMiddleware` generic preserves the middleware tuple as a literal type,
|
|
1023
|
+
* enabling TypeScript to extract and intersect `Variables` from each middleware
|
|
1024
|
+
* element onto the handler's `ctx` type.
|
|
1025
|
+
*
|
|
1026
|
+
* @param def - Command definition including description, args schema, middleware, and handler.
|
|
1026
1027
|
* @returns A resolved Command object for registration in the command map.
|
|
1027
1028
|
*/
|
|
1028
1029
|
function command(def) {
|