@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.
Files changed (51) hide show
  1. package/dist/{config-BvGapuFJ.js → config-Db_sjFU-.js} +60 -65
  2. package/dist/config-Db_sjFU-.js.map +1 -0
  3. package/dist/create-http-client-tZJWlWp1.js +165 -0
  4. package/dist/create-http-client-tZJWlWp1.js.map +1 -0
  5. package/dist/{create-store-BQUX0tAn.js → create-store-D-fQpCql.js} +32 -4
  6. package/dist/create-store-D-fQpCql.js.map +1 -0
  7. package/dist/index.d.ts +21 -6
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +18 -17
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/config.js +2 -2
  12. package/dist/lib/project.d.ts +1 -1
  13. package/dist/lib/project.d.ts.map +1 -1
  14. package/dist/lib/project.js +1 -1
  15. package/dist/lib/store.d.ts +2 -1
  16. package/dist/lib/store.d.ts.map +1 -1
  17. package/dist/lib/store.js +2 -2
  18. package/dist/middleware/auth.d.ts +223 -14
  19. package/dist/middleware/auth.d.ts.map +1 -1
  20. package/dist/middleware/auth.js +973 -408
  21. package/dist/middleware/auth.js.map +1 -1
  22. package/dist/middleware/http.d.ts +10 -16
  23. package/dist/middleware/http.d.ts.map +1 -1
  24. package/dist/middleware/http.js +21 -221
  25. package/dist/middleware/http.js.map +1 -1
  26. package/dist/{middleware-D3psyhYo.js → middleware-BFBKNSPQ.js} +13 -2
  27. package/dist/{middleware-D3psyhYo.js.map → middleware-BFBKNSPQ.js.map} +1 -1
  28. package/dist/{project-NPtYX2ZX.js → project-DuXgjaa_.js} +19 -16
  29. package/dist/project-DuXgjaa_.js.map +1 -0
  30. package/dist/{types-kjpRau0U.d.ts → types-BaZ5WqVM.d.ts} +78 -13
  31. package/dist/types-BaZ5WqVM.d.ts.map +1 -0
  32. package/dist/{types-Cz9h927W.d.ts → types-C0CYivzY.d.ts} +1 -1
  33. package/dist/{types-Cz9h927W.d.ts.map → types-C0CYivzY.d.ts.map} +1 -1
  34. package/package.json +5 -12
  35. package/dist/config-BvGapuFJ.js.map +0 -1
  36. package/dist/create-store-BQUX0tAn.js.map +0 -1
  37. package/dist/lib/output.d.ts +0 -62
  38. package/dist/lib/output.d.ts.map +0 -1
  39. package/dist/lib/output.js +0 -276
  40. package/dist/lib/output.js.map +0 -1
  41. package/dist/lib/prompts.d.ts +0 -24
  42. package/dist/lib/prompts.d.ts.map +0 -1
  43. package/dist/lib/prompts.js +0 -3
  44. package/dist/project-NPtYX2ZX.js.map +0 -1
  45. package/dist/prompts-lLfUSgd6.js +0 -63
  46. package/dist/prompts-lLfUSgd6.js.map +0 -1
  47. package/dist/types-CqKJhsYk.d.ts +0 -135
  48. package/dist/types-CqKJhsYk.d.ts.map +0 -1
  49. package/dist/types-DFtYg5uZ.d.ts +0 -26
  50. package/dist/types-DFtYg5uZ.d.ts.map +0 -1
  51. package/dist/types-kjpRau0U.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","names":["match"],"sources":["../../src/middleware/http/build-auth-headers.ts","../../src/middleware/http/create-http-client.ts","../../src/middleware/http/http.ts"],"sourcesContent":["/**\n * Convert auth credentials into HTTP headers.\n *\n * Uses exhaustive pattern matching to map each credential variant to\n * the appropriate header format.\n *\n * @module\n */\n\nimport { Buffer } from 'node:buffer'\n\nimport { match } from 'ts-pattern'\n\nimport type { AuthCredential } from '../auth/types.js'\n\n/**\n * Convert an auth credential into HTTP headers.\n *\n * @param credential - The credential to convert.\n * @returns A record of header name to header value.\n */\nexport function buildAuthHeaders(credential: AuthCredential): Readonly<Record<string, string>> {\n return match(credential)\n .with({ type: 'bearer' }, (c) => ({\n Authorization: `Bearer ${c.token}`,\n }))\n .with({ type: 'basic' }, (c) => ({\n Authorization: `Basic ${Buffer.from(`${c.username}:${c.password}`).toString('base64')}`,\n }))\n .with({ type: 'api-key' }, (c) => ({\n [c.headerName]: c.key,\n }))\n .with({ type: 'custom' }, (c) => ({ ...c.headers }))\n .exhaustive()\n}\n","/**\n * Typed HTTP client factory.\n *\n * Creates a closure-based {@link HttpClient} with pre-configured base URL,\n * auth credentials, and default headers. All methods delegate to a shared\n * request executor.\n *\n * @module\n */\n\nimport { attemptAsync } from '@kidd-cli/utils/fp'\n\nimport type { AuthCredential } from '../auth/types.js'\nimport { buildAuthHeaders } from './build-auth-headers.js'\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 credential?: AuthCredential\n readonly defaultHeaders?: Readonly<Record<string, string>>\n}\n\n/**\n * Create a typed HTTP client with pre-configured base URL, auth, and headers.\n *\n * @param options - Client configuration.\n * @returns An HttpClient instance.\n */\nexport function createHttpClient(options: CreateHttpClientOptions): HttpClient {\n const { baseUrl, credential, defaultHeaders } = options\n\n return {\n delete: <TResponse = unknown>(path: string, requestOptions?: RequestOptions) =>\n executeRequest<TResponse>(\n baseUrl,\n 'DELETE',\n path,\n credential,\n defaultHeaders,\n requestOptions\n ),\n\n get: <TResponse = unknown>(path: string, requestOptions?: RequestOptions) =>\n executeRequest<TResponse>(baseUrl, 'GET', path, credential, defaultHeaders, requestOptions),\n\n patch: <TResponse = unknown, TBody = unknown>(\n path: string,\n requestOptions?: RequestOptions<TBody>\n ) =>\n executeRequest<TResponse>(baseUrl, 'PATCH', path, credential, defaultHeaders, requestOptions),\n\n post: <TResponse = unknown, TBody = unknown>(\n path: string,\n requestOptions?: RequestOptions<TBody>\n ) =>\n executeRequest<TResponse>(baseUrl, 'POST', path, credential, defaultHeaders, requestOptions),\n\n put: <TResponse = unknown, TBody = unknown>(\n path: string,\n requestOptions?: RequestOptions<TBody>\n ) =>\n executeRequest<TResponse>(baseUrl, 'PUT', path, credential, defaultHeaders, 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 * Resolve auth headers from a credential, returning an empty record\n * when no credential is provided.\n *\n * @private\n * @param credential - Optional auth credential.\n * @returns A record of auth headers.\n */\nfunction resolveAuthHeaders(\n credential: AuthCredential | undefined\n): Readonly<Record<string, string>> {\n if (credential !== undefined) {\n return buildAuthHeaders(credential)\n }\n\n return {}\n}\n\n/**\n * Merge auth, default, and per-request headers into a single record.\n *\n * Per-request headers take highest priority, then default headers,\n * then auth headers.\n *\n * @private\n * @param credential - Optional auth credential.\n * @param defaultHeaders - Optional default headers.\n * @param requestHeaders - Optional per-request headers.\n * @returns The merged headers record.\n */\nfunction mergeHeaders(\n credential: AuthCredential | undefined,\n defaultHeaders: Readonly<Record<string, string>> | undefined,\n requestHeaders: Readonly<Record<string, string>> | undefined\n): Readonly<Record<string, string>> {\n const authHeaders = resolveAuthHeaders(credential)\n\n return {\n ...authHeaders,\n ...defaultHeaders,\n ...requestHeaders,\n }\n}\n\n/**\n * Extract the params field from request options if present.\n *\n * @private\n * @param options - Optional per-request options.\n * @returns The params record or undefined.\n */\nfunction extractParams(\n options: RequestOptions | undefined\n): Readonly<Record<string, string>> | undefined {\n if (options !== undefined) {\n return options.params\n }\n\n return undefined\n}\n\n/**\n * Extract the headers field from request options if present.\n *\n * @private\n * @param options - Optional per-request options.\n * @returns The headers record or undefined.\n */\nfunction extractHeaders(\n options: RequestOptions | undefined\n): Readonly<Record<string, string>> | undefined {\n if (options !== undefined) {\n return options.headers\n }\n\n return undefined\n}\n\n/**\n * Extract the signal field from request options if present.\n *\n * @private\n * @param options - Optional per-request options.\n * @returns The AbortSignal or undefined.\n */\nfunction extractSignal(options: RequestOptions | undefined): AbortSignal | undefined {\n if (options !== undefined) {\n return options.signal\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 credential - Optional auth credential.\n * @param defaultHeaders - Optional default headers.\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 credential: AuthCredential | undefined,\n defaultHeaders: Readonly<Record<string, string>> | undefined,\n options: RequestOptions | undefined\n): Promise<TypedResponse<TResponse>> {\n const url = buildUrl(baseUrl, path, extractParams(options))\n const headers = mergeHeaders(credential, defaultHeaders, extractHeaders(options))\n const body = resolveBody(options)\n const signal = extractSignal(options)\n const init = buildFetchInit(method, headers, body, 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","/**\n * HTTP client middleware factory.\n *\n * Creates a middleware that decorates the context with a typed\n * {@link HttpClient} bound to a base URL and optional auth credentials.\n *\n * @module\n */\n\nimport { decorateContext } from '@/context/decorate.js'\nimport type { Context } from '@/context/types.js'\nimport { middleware } from '@/middleware.js'\nimport type { Middleware } from '@/types.js'\n\nimport type { AuthCredential } from '../auth/types.js'\nimport { createHttpClient } from './create-http-client.js'\nimport type { HttpOptions } from './types.js'\n\n/**\n * Create an HTTP client middleware that decorates the context\n * with a typed client.\n *\n * Reads auth credentials from `ctx.auth.credential()` (set by the auth\n * middleware), builds a typed {@link HttpClient}, and attaches it to\n * `ctx[namespace]`.\n *\n * @param options - HTTP middleware configuration.\n * @returns A Middleware that adds an HttpClient to ctx[namespace].\n */\nexport function http(options: HttpOptions): Middleware {\n const { namespace, baseUrl, defaultHeaders } = options\n\n return middleware(async (ctx, next) => {\n const credential = resolveCredential(ctx)\n\n const client = createHttpClient({\n baseUrl,\n credential,\n defaultHeaders,\n })\n\n decorateContext(ctx, namespace, client)\n\n await next()\n })\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the auth credential from the context.\n *\n * Calls `ctx.auth.credential()` when the auth middleware has run.\n * Returns undefined when no auth context is available.\n *\n * @private\n * @param ctx - The context object.\n * @returns The credential or undefined.\n */\nfunction resolveCredential(ctx: Context): AuthCredential | undefined {\n if (ctx.auth === undefined) {\n return undefined\n }\n\n const cred = ctx.auth.credential()\n\n if (cred === null) {\n return undefined\n }\n\n return cred\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"http.js","names":[],"sources":["../../src/middleware/http/http.ts"],"sourcesContent":["/**\n * HTTP client middleware factory.\n *\n * Creates a middleware that decorates the context with a typed\n * {@link HttpClient} bound to a base URL and optional headers.\n *\n * This middleware is fully decoupled from auth. For automatic credential\n * injection, use `auth({ http: { ... } })` instead.\n *\n * @module\n */\n\nimport { decorateContext } from '@/context/decorate.js'\nimport type { Context } from '@/context/types.js'\nimport { middleware } from '@/middleware.js'\nimport type { Middleware } from '@/types.js'\n\nimport { createHttpClient } from './create-http-client.js'\nimport type { HttpOptions } from './types.js'\n\n/**\n * Create an HTTP client middleware that decorates the context\n * with a typed client.\n *\n * Resolves headers from the `headers` option (static record or function),\n * builds a typed {@link HttpClient}, and attaches it to `ctx[namespace]`.\n *\n * @param options - HTTP middleware configuration.\n * @returns A Middleware that adds an HttpClient to ctx[namespace].\n */\nexport function http(options: HttpOptions): Middleware {\n const { namespace, baseUrl, headers } = options\n\n return middleware((ctx, next) => {\n const resolvedHeaders = resolveHeaders(ctx, headers)\n\n const client = createHttpClient({\n baseUrl,\n defaultHeaders: resolvedHeaders,\n })\n\n decorateContext(ctx, namespace, client)\n\n return next()\n })\n}\n\n// ---------------------------------------------------------------------------\n// Private helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve headers from the options value.\n *\n * Calls the function form with ctx when provided, returns static headers\n * directly, or returns undefined when no headers are configured.\n *\n * @private\n * @param ctx - The context object.\n * @param headers - The headers option (static, function, or undefined).\n * @returns The resolved headers record or undefined.\n */\nfunction resolveHeaders(\n ctx: Context,\n headers: HttpOptions['headers']\n): Readonly<Record<string, string>> | undefined {\n if (headers === undefined) {\n return undefined\n }\n\n if (typeof headers === 'function') {\n return headers(ctx)\n }\n\n return headers\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,KAAK,SAAkC;CACrD,MAAM,EAAE,WAAW,SAAS,YAAY;AAExC,QAAO,YAAY,KAAK,SAAS;AAQ/B,kBAAgB,KAAK,WALN,iBAAiB;GAC9B;GACA,gBAJsB,eAAe,KAAK,QAAQ;GAKnD,CAAC,CAEqC;AAEvC,SAAO,MAAM;GACb;;;;;;;;;;;;;AAkBJ,SAAS,eACP,KACA,SAC8C;AAC9C,KAAI,YAAY,OACd;AAGF,KAAI,OAAO,YAAY,WACrB,QAAO,QAAQ,IAAI;AAGrB,QAAO"}
@@ -42,8 +42,19 @@ function decorateContext(ctx, key, value) {
42
42
  /**
43
43
  * Create a typed middleware that runs before command handlers.
44
44
  *
45
+ * Use the generic parameter to declare context variables the middleware provides.
46
+ * The handler's `ctx` type in downstream commands will include these variables.
47
+ *
45
48
  * @param handler - The middleware function receiving ctx and next.
46
- * @returns A Middleware object for use in the cli() middleware stack.
49
+ * @returns A Middleware object for use in the cli() or command() middleware stack.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const loadUser = middleware<{ Variables: { user: User } }>(async (ctx, next) => {
54
+ * decorateContext(ctx, 'user', await fetchUser())
55
+ * await next()
56
+ * })
57
+ * ```
47
58
  */
48
59
  function middleware(handler) {
49
60
  return withTag({ handler }, "Middleware");
@@ -51,4 +62,4 @@ function middleware(handler) {
51
62
 
52
63
  //#endregion
53
64
  export { decorateContext as n, middleware as t };
54
- //# sourceMappingURL=middleware-D3psyhYo.js.map
65
+ //# sourceMappingURL=middleware-BFBKNSPQ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware-D3psyhYo.js","names":[],"sources":["../src/context/decorate.ts","../src/middleware.ts"],"sourcesContent":["import type { Context } from './types.js'\n\n/**\n * Add a typed, immutable property to a context instance.\n *\n * Middleware authors use this to extend ctx with custom properties.\n * Pair with module augmentation on Context for type safety:\n *\n * ```ts\n * declare module '@kidd-cli/core' {\n * interface Context {\n * readonly github: HttpClient\n * }\n * }\n * ```\n *\n * **Note:** This function mutates the context object via\n * `Object.defineProperty`. The added property is non-writable and\n * non-configurable, making it effectively frozen after assignment.\n * Mutation is intentional here — the context is assembled incrementally\n * across middleware, and copying the entire object on each decoration\n * would break the single-reference threading model used by the runner.\n *\n * @param ctx - The context instance to decorate (mutated in place).\n * @param key - The property name.\n * @param value - The property value (frozen after assignment).\n * @returns The same ctx reference, now carrying the new property.\n */\nexport function decorateContext<TKey extends string, TValue>(\n ctx: Context,\n key: TKey,\n value: TValue\n): Context {\n Object.defineProperty(ctx, key, { configurable: false, enumerable: true, value, writable: false })\n return ctx\n}\n","import { withTag } from '@kidd-cli/utils/tag'\n\nimport type { Middleware, MiddlewareFn } from './types.js'\n\n/**\n * Create a typed middleware that runs before command handlers.\n *\n * @param handler - The middleware function receiving ctx and next.\n * @returns A Middleware object for use in the cli() middleware stack.\n */\nexport function middleware<TConfig extends Record<string, unknown> = Record<string, unknown>>(\n handler: MiddlewareFn<TConfig>\n): Middleware<TConfig> {\n return withTag({ handler }, 'Middleware')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,gBACd,KACA,KACA,OACS;AACT,QAAO,eAAe,KAAK,KAAK;EAAE,cAAc;EAAO,YAAY;EAAM;EAAO,UAAU;EAAO,CAAC;AAClG,QAAO;;;;;;;;;;;ACxBT,SAAgB,WACd,SACqB;AACrB,QAAO,QAAQ,EAAE,SAAS,EAAE,aAAa"}
1
+ {"version":3,"file":"middleware-BFBKNSPQ.js","names":[],"sources":["../src/context/decorate.ts","../src/middleware.ts"],"sourcesContent":["import type { Context } from './types.js'\n\n/**\n * Add a typed, immutable property to a context instance.\n *\n * Middleware authors use this to extend ctx with custom properties.\n * Pair with module augmentation on Context for type safety:\n *\n * ```ts\n * declare module '@kidd-cli/core' {\n * interface Context {\n * readonly github: HttpClient\n * }\n * }\n * ```\n *\n * **Note:** This function mutates the context object via\n * `Object.defineProperty`. The added property is non-writable and\n * non-configurable, making it effectively frozen after assignment.\n * Mutation is intentional here — the context is assembled incrementally\n * across middleware, and copying the entire object on each decoration\n * would break the single-reference threading model used by the runner.\n *\n * @param ctx - The context instance to decorate (mutated in place).\n * @param key - The property name.\n * @param value - The property value (frozen after assignment).\n * @returns The same ctx reference, now carrying the new property.\n */\nexport function decorateContext<TKey extends string, TValue>(\n ctx: Context,\n key: TKey,\n value: TValue\n): Context {\n Object.defineProperty(ctx, key, { configurable: false, enumerable: true, value, writable: false })\n return ctx\n}\n","import { withTag } from '@kidd-cli/utils/tag'\n\nimport type { Middleware, MiddlewareEnv, MiddlewareFn } from './types.js'\n\n/**\n * Create a typed middleware that runs before command handlers.\n *\n * Use the generic parameter to declare context variables the middleware provides.\n * The handler's `ctx` type in downstream commands will include these variables.\n *\n * @param handler - The middleware function receiving ctx and next.\n * @returns A Middleware object for use in the cli() or command() middleware stack.\n *\n * @example\n * ```ts\n * const loadUser = middleware<{ Variables: { user: User } }>(async (ctx, next) => {\n * decorateContext(ctx, 'user', await fetchUser())\n * await next()\n * })\n * ```\n */\nexport function middleware<TEnv extends MiddlewareEnv = MiddlewareEnv>(\n handler: MiddlewareFn<TEnv>\n): Middleware<TEnv> {\n return withTag({ handler }, 'Middleware')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,gBACd,KACA,KACA,OACS;AACT,QAAO,eAAe,KAAK,KAAK;EAAE,cAAc;EAAO,YAAY;EAAM;EAAO,UAAU;EAAO,CAAC;AAClG,QAAO;;;;;;;;;;;;;;;;;;;;;;ACbT,SAAgB,WACd,SACkB;AAClB,QAAO,QAAQ,EAAE,SAAS,EAAE,aAAa"}
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
5
5
  import { existsSync, readFileSync, statSync } from "node:fs";
6
6
 
7
7
  //#region src/lib/project/root.ts
8
+ const GITDIR_RE = /^gitdir:\s*(.+)$/;
8
9
  const MIN_MODULES_PARTS = 2;
9
10
  /**
10
11
  * Walk up the directory tree to find the nearest git project root.
@@ -52,22 +53,34 @@ function isInSubmodule(startDir) {
52
53
  function getParentRepoRoot(startDir) {
53
54
  const projectRoot = findProjectRoot(startDir);
54
55
  if (!projectRoot || !projectRoot.isSubmodule) return null;
55
- const gitFileContent = resolveGitDirFromFile(join(projectRoot.path, ".git"));
56
+ const gitFileContent = readGitFile(join(projectRoot.path, ".git"));
56
57
  if (gitFileContent === null) return null;
57
58
  return resolveParentGitDir(projectRoot, gitFileContent);
58
59
  }
59
60
  /**
61
+ * Read and trim the contents of a git-related file.
62
+ *
63
+ * @param filePath - The absolute file path to read.
64
+ * @returns The trimmed file content, or null when the file cannot be read.
65
+ * @private
66
+ */
67
+ function readGitFile(filePath) {
68
+ const [error, content] = attempt(() => readFileSync(filePath, "utf8"));
69
+ if (error || content === null) return null;
70
+ return content.trim();
71
+ }
72
+ /**
60
73
  * Resolve a `.git` file reference to determine if this is a submodule.
61
74
  *
62
75
  * @private
63
76
  */
64
77
  function resolveGitFileSubmodule(gitPath, currentDir) {
65
- const [readError, gitFileContent] = attempt(() => readFileSync(gitPath, "utf8").trim());
66
- if (readError || gitFileContent === null) return {
78
+ const gitFileContent = readGitFile(gitPath);
79
+ if (gitFileContent === null) return {
67
80
  isSubmodule: false,
68
81
  path: currentDir
69
82
  };
70
- const gitDirMatch = gitFileContent.match(/^gitdir:\s*(.+)$/);
83
+ const gitDirMatch = gitFileContent.match(GITDIR_RE);
71
84
  if (gitDirMatch && gitDirMatch[1]) {
72
85
  const gitDir = resolve(currentDir, gitDirMatch[1]);
73
86
  return {
@@ -93,16 +106,6 @@ function checkGitPath(gitPath, currentDir) {
93
106
  return null;
94
107
  }
95
108
  /**
96
- * Read a `.git` file and return its raw content.
97
- *
98
- * @private
99
- */
100
- function resolveGitDirFromFile(gitFilePath) {
101
- const [readError, gitFileContent] = attempt(() => readFileSync(gitFilePath, "utf8").trim());
102
- if (readError || gitFileContent === null) return null;
103
- return gitFileContent;
104
- }
105
- /**
106
109
  * Extract the parent repository root from a resolved git modules path.
107
110
  *
108
111
  * @private
@@ -121,7 +124,7 @@ function resolveParentFromGitDir(resolvedGitDir) {
121
124
  * @private
122
125
  */
123
126
  function resolveParentGitDir(projectRoot, gitFileContent) {
124
- const gitDirMatch = gitFileContent.match(/^gitdir:\s*(.+)$/);
127
+ const gitDirMatch = gitFileContent.match(GITDIR_RE);
125
128
  if (!gitDirMatch) return null;
126
129
  const gitDir = gitDirMatch[1] ?? "";
127
130
  const resolvedGitDir = resolve(projectRoot.path, gitDir);
@@ -178,4 +181,4 @@ function resolvePath(options) {
178
181
 
179
182
  //#endregion
180
183
  export { getParentRepoRoot as a, findProjectRoot as i, resolveLocalPath as n, isInSubmodule as o, resolvePath as r, resolveGlobalPath as t };
181
- //# sourceMappingURL=project-NPtYX2ZX.js.map
184
+ //# sourceMappingURL=project-DuXgjaa_.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-DuXgjaa_.js","names":["match"],"sources":["../src/lib/project/root.ts","../src/lib/project/paths.ts"],"sourcesContent":["import { existsSync, readFileSync, statSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\n\nimport { attempt } from '@kidd-cli/utils/fp'\n\nimport type { ProjectRoot } from './types.js'\n\nconst GITDIR_RE = /^gitdir:\\s*(.+)$/\nconst MIN_MODULES_PARTS = 2\n\n/**\n * Walk up the directory tree to find the nearest git project root.\n *\n * @param startDir - Directory to start searching from (defaults to cwd).\n * @returns The project root info, or null if no git root is found.\n */\nexport function findProjectRoot(startDir: string = process.cwd()): ProjectRoot | null {\n /**\n * Recursively walk up the directory tree searching for a `.git` marker.\n *\n * @private\n */\n const findRootRecursive = (currentDir: string, visited: Set<string>): ProjectRoot | null => {\n if (visited.has(currentDir)) {\n return null\n }\n const nextVisited = new Set([...visited, currentDir])\n\n const gitPath = join(currentDir, '.git')\n try {\n const result = checkGitPath(gitPath, currentDir)\n if (result) {\n return result\n }\n } catch {\n // Race condition: file may have been deleted between existsSync and statSync\n }\n\n const parent = dirname(currentDir)\n if (parent === currentDir) {\n return null\n }\n return findRootRecursive(parent, nextVisited)\n }\n\n return findRootRecursive(resolve(startDir), new Set())\n}\n\n/**\n * Check whether the current directory is inside a git submodule.\n *\n * @param startDir - Directory to start searching from.\n * @returns True if the directory is inside a submodule.\n */\nexport function isInSubmodule(startDir?: string): boolean {\n const projectRoot = findProjectRoot(startDir)\n if (!projectRoot) {\n return false\n }\n return projectRoot.isSubmodule\n}\n\n/**\n * Resolve the parent repository root when inside a git submodule.\n *\n * @param startDir - Directory to start searching from.\n * @returns The parent repository root path, or null.\n */\nexport function getParentRepoRoot(startDir?: string): string | null {\n const projectRoot = findProjectRoot(startDir)\n if (!projectRoot || !projectRoot.isSubmodule) {\n return null\n }\n\n const gitFilePath = join(projectRoot.path, '.git')\n const gitFileContent = readGitFile(gitFilePath)\n if (gitFileContent === null) {\n return null\n }\n\n return resolveParentGitDir(projectRoot, gitFileContent)\n}\n\n// ---------------------------------------------------------------------------\n// Private\n// ---------------------------------------------------------------------------\n\n/**\n * Read and trim the contents of a git-related file.\n *\n * @param filePath - The absolute file path to read.\n * @returns The trimmed file content, or null when the file cannot be read.\n * @private\n */\nfunction readGitFile(filePath: string): string | null {\n const [error, content] = attempt(() => readFileSync(filePath, 'utf8'))\n if (error || content === null) {\n return null\n }\n return content.trim()\n}\n\n/**\n * Resolve a `.git` file reference to determine if this is a submodule.\n *\n * @private\n */\nfunction resolveGitFileSubmodule(gitPath: string, currentDir: string): ProjectRoot | null {\n const gitFileContent = readGitFile(gitPath)\n if (gitFileContent === null) {\n return { isSubmodule: false, path: currentDir }\n }\n const gitDirMatch = gitFileContent.match(GITDIR_RE)\n if (gitDirMatch && gitDirMatch[1]) {\n const gitDir = resolve(currentDir, gitDirMatch[1])\n const isSubmodule = /[/\\\\]\\.git[/\\\\]modules[/\\\\]/.test(gitDir)\n return { isSubmodule, path: currentDir }\n }\n return null\n}\n\n/**\n * Check whether a `.git` path is a directory or file and resolve accordingly.\n *\n * @private\n */\nfunction checkGitPath(gitPath: string, currentDir: string): ProjectRoot | null {\n if (!existsSync(gitPath)) {\n return null\n }\n\n const stats = statSync(gitPath)\n if (stats.isDirectory()) {\n return { isSubmodule: false, path: currentDir }\n }\n\n if (stats.isFile()) {\n return resolveGitFileSubmodule(gitPath, currentDir)\n }\n\n return null\n}\n\n/**\n * Extract the parent repository root from a resolved git modules path.\n *\n * @private\n */\nfunction resolveParentFromGitDir(resolvedGitDir: string): string | null {\n const gitDirParts = resolvedGitDir.split('/modules/')\n if (gitDirParts.length >= MIN_MODULES_PARTS) {\n const [parentGitDir] = gitDirParts\n if (parentGitDir && parentGitDir.endsWith('.git')) {\n return dirname(parentGitDir)\n }\n }\n return null\n}\n\n/**\n * Resolve the parent repository root from a submodule's gitdir reference.\n *\n * @private\n */\nfunction resolveParentGitDir(projectRoot: ProjectRoot, gitFileContent: string): string | null {\n const gitDirMatch = gitFileContent.match(GITDIR_RE)\n if (!gitDirMatch) {\n return null\n }\n\n const gitDir = gitDirMatch[1] ?? ''\n const resolvedGitDir = resolve(projectRoot.path, gitDir)\n\n if (process.platform === 'win32') {\n return null\n }\n\n return resolveParentFromGitDir(resolvedGitDir)\n}\n","import { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nimport { match } from 'ts-pattern'\n\nimport { findProjectRoot } from './root.js'\nimport type { ResolvePathOptions } from './types.js'\n\n/**\n * Resolve a directory path relative to the project root.\n *\n * @param options - Options containing the directory name and optional start directory.\n * @returns The resolved local path, or null if no project root is found.\n */\nexport function resolveLocalPath(options: {\n readonly dirName: string\n readonly startDir?: string\n}): string | null {\n const projectRoot = findProjectRoot(options.startDir)\n if (!projectRoot) {\n return null\n }\n return join(projectRoot.path, options.dirName)\n}\n\n/**\n * Resolve a directory path relative to the user's home directory.\n *\n * @param options - Options containing the directory name.\n * @returns The resolved global path.\n */\nexport function resolveGlobalPath(options: { readonly dirName: string }): string {\n return join(homedir(), options.dirName)\n}\n\n/**\n * Resolve a directory path using the specified source strategy.\n *\n * When source is 'local', resolves relative to the project root.\n * When source is 'global', resolves relative to the home directory.\n * When source is 'resolve' (default), tries local first, falling back to global.\n *\n * @param options - Resolution options with dirName, source, and startDir.\n * @returns The resolved path, or null if local resolution fails with source='local'.\n */\nexport function resolvePath(options: ResolvePathOptions): string | null {\n const { dirName, source = 'resolve', startDir } = options\n return match(source)\n .with('local', (): string | null => resolveLocalPath({ dirName, startDir }))\n .with('global', (): string => resolveGlobalPath({ dirName }))\n .with('resolve', (): string => {\n const localPath = resolveLocalPath({ dirName, startDir })\n if (localPath) {\n return localPath\n }\n return resolveGlobalPath({ dirName })\n })\n .exhaustive()\n}\n"],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  import { t as CliLogger } from "./logger-BkQQej8h.js";
2
- import { n as Spinner } from "./types-DFtYg5uZ.js";
3
2
  import { Tagged } from "@kidd-cli/utils/tag";
4
3
  import { z } from "zod";
5
4
 
@@ -96,6 +95,14 @@ interface Prompts {
96
95
  password(opts: TextOptions): Promise<string>;
97
96
  }
98
97
  /**
98
+ * Terminal spinner for indicating long-running operations.
99
+ */
100
+ interface Spinner {
101
+ start(message?: string): void;
102
+ stop(message?: string): void;
103
+ message(message: string): void;
104
+ }
105
+ /**
99
106
  * Formatting options for structured output methods.
100
107
  */
101
108
  interface OutputOptions {
@@ -235,18 +242,68 @@ type AnyRecord = Record<string, unknown>;
235
242
  */
236
243
  type DeepReadonly<TType> = TType extends ((...args: unknown[]) => unknown) ? TType : TType extends readonly (infer TItem)[] ? readonly DeepReadonly<TItem>[] : TType extends object ? { readonly [Key in keyof TType]: DeepReadonly<TType[Key]> } : TType;
237
244
  /**
245
+ * Detects the `any` type using the intersection trick.
246
+ * `0 extends 1 & T` is only true when T is `any`.
247
+ */
248
+ type IsAny<T> = 0 extends 1 & T ? true : false;
249
+ /**
250
+ * Converts a union `A | B | C` to an intersection `A & B & C`
251
+ * via the standard contravariant trick.
252
+ */
253
+ type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never;
254
+ /**
255
+ * Environment descriptor for typed middleware.
256
+ * Middleware declares the context variables it provides via the `Variables` property.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * middleware<{ Variables: { user: User } }>(async (ctx, next) => {
261
+ * decorateContext(ctx, 'user', await fetchUser())
262
+ * await next()
263
+ * })
264
+ * ```
265
+ */
266
+ interface MiddlewareEnv {
267
+ readonly Variables?: AnyRecord;
268
+ }
269
+ /**
270
+ * Extracts the `Variables` from a {@link MiddlewareEnv}, guarding against `any`.
271
+ * Returns an empty object when `TEnv` is `any` or has no `Variables`.
272
+ */
273
+ type ExtractVariables<TEnv extends MiddlewareEnv> = IsAny<TEnv> extends true ? {} : TEnv extends {
274
+ readonly Variables: infer TVars extends AnyRecord;
275
+ } ? TVars : {};
276
+ /**
277
+ * Extracts the `TEnv` type parameter from a {@link Middleware} instance.
278
+ */
279
+ type MiddlewareEnvOf<T> = T extends Middleware<infer TEnv> ? TEnv : MiddlewareEnv;
280
+ /**
281
+ * Walks a readonly middleware tuple and intersects all `Variables` from each element.
282
+ * Produces the merged context variables type for a command handler.
283
+ *
284
+ * @example
285
+ * ```ts
286
+ * type Vars = InferVariables<[Middleware<{ Variables: { user: User } }>, Middleware<{ Variables: { org: Org } }>]>
287
+ * // { user: User } & { org: Org }
288
+ * ```
289
+ */
290
+ type InferVariables<TMiddleware extends readonly Middleware<MiddlewareEnv>[]> = UnionToIntersection<ExtractVariables<MiddlewareEnvOf<TMiddleware[number]>>>;
291
+ /**
238
292
  * The next() function passed to middleware. Call it to continue to the next middleware or handler.
239
293
  */
240
294
  type NextFunction = () => Promise<void>;
241
295
  /**
242
296
  * A middleware function receives ctx and next.
297
+ *
298
+ * The `_TEnv` generic is phantom — it carries the environment type through
299
+ * {@link Middleware} for type inference without affecting the runtime signature.
243
300
  */
244
- type MiddlewareFn<TConfig extends AnyRecord = AnyRecord> = (ctx: Context<AnyRecord, TConfig>, next: NextFunction) => Promise<void> | void;
301
+ type MiddlewareFn<_TEnv extends MiddlewareEnv = MiddlewareEnv> = (ctx: Context, next: NextFunction) => Promise<void> | void;
245
302
  /**
246
303
  * A middleware object wrapping a MiddlewareFn. Returned by the middleware() factory.
247
304
  */
248
- type Middleware<TConfig extends AnyRecord = AnyRecord> = Tagged<{
249
- readonly handler: MiddlewareFn<TConfig>;
305
+ type Middleware<TEnv extends MiddlewareEnv = MiddlewareEnv> = Tagged<{
306
+ readonly handler: MiddlewareFn<TEnv>;
250
307
  }, "Middleware">;
251
308
  /**
252
309
  * Yargs-native arg format -- accepted as an alternative to zod.
@@ -279,12 +336,20 @@ type YargsArgBaseType<TType extends string> = TType extends "string" ? string :
279
336
  type InferArgs<TDef extends ArgsDef> = TDef extends z.ZodObject<z.ZodRawShape> ? z.infer<TDef> : TDef extends Record<string, YargsArgDef> ? { [Key in keyof TDef]: YargsArgValue<TDef[Key]> } : AnyRecord;
280
337
  /**
281
338
  * Handler function for a command. Receives the fully typed context.
339
+ *
340
+ * @typeParam TArgs - Parsed args type.
341
+ * @typeParam TConfig - Config type.
342
+ * @typeParam TVars - Context variables contributed by typed middleware.
282
343
  */
283
- type HandlerFn<TArgs extends AnyRecord = AnyRecord, TConfig extends AnyRecord = AnyRecord> = (ctx: Context<TArgs, TConfig>) => Promise<void> | void;
344
+ type HandlerFn<TArgs extends AnyRecord = AnyRecord, TConfig extends AnyRecord = AnyRecord, TVars = {}> = (ctx: Context<TArgs, TConfig> & Readonly<TVars>) => Promise<void> | void;
284
345
  /**
285
346
  * Options passed to `command()`.
347
+ *
348
+ * @typeParam TArgsDef - Arg definitions type.
349
+ * @typeParam TConfig - Config type.
350
+ * @typeParam TMiddleware - Tuple of typed middleware, preserving per-element `TEnv`.
286
351
  */
287
- interface CommandDef<TArgsDef extends ArgsDef = ArgsDef, TConfig extends AnyRecord = AnyRecord> {
352
+ interface CommandDef<TArgsDef extends ArgsDef = ArgsDef, TConfig extends AnyRecord = AnyRecord, TMiddleware extends readonly Middleware<MiddlewareEnv>[] = readonly Middleware<MiddlewareEnv>[]> {
288
353
  /**
289
354
  * Human-readable description shown in help text.
290
355
  */
@@ -296,7 +361,7 @@ interface CommandDef<TArgsDef extends ArgsDef = ArgsDef, TConfig extends AnyReco
296
361
  /**
297
362
  * Command-level middleware. Runs inside the root middleware chain, wrapping the handler.
298
363
  */
299
- middleware?: Middleware[];
364
+ middleware?: TMiddleware;
300
365
  /**
301
366
  * Nested subcommands — a static map or a promise from `autoload()`.
302
367
  */
@@ -304,17 +369,17 @@ interface CommandDef<TArgsDef extends ArgsDef = ArgsDef, TConfig extends AnyReco
304
369
  /**
305
370
  * The command handler.
306
371
  */
307
- handler?: HandlerFn<TArgsDef extends z.ZodObject<z.ZodRawShape> ? z.infer<TArgsDef> : InferArgs<TArgsDef & ArgsDef>, TConfig>;
372
+ handler?: HandlerFn<TArgsDef extends z.ZodObject<z.ZodRawShape> ? z.infer<TArgsDef> : InferArgs<TArgsDef & ArgsDef>, TConfig, InferVariables<TMiddleware>>;
308
373
  }
309
374
  /**
310
375
  * A resolved command object. Returned by command().
311
376
  */
312
- type Command<TArgsDef extends ArgsDef = ArgsDef, TConfig extends AnyRecord = AnyRecord> = Tagged<{
377
+ type Command<TArgsDef extends ArgsDef = ArgsDef, TConfig extends AnyRecord = AnyRecord, TMiddleware extends readonly Middleware<MiddlewareEnv>[] = readonly Middleware<MiddlewareEnv>[]> = Tagged<{
313
378
  readonly description?: string;
314
379
  readonly args?: TArgsDef;
315
- readonly middleware?: Middleware[];
380
+ readonly middleware?: TMiddleware;
316
381
  readonly commands?: CommandMap | Promise<CommandMap>;
317
- readonly handler?: HandlerFn<TArgsDef extends z.ZodObject<z.ZodRawShape> ? z.infer<TArgsDef> : InferArgs<TArgsDef & ArgsDef>, TConfig>;
382
+ readonly handler?: HandlerFn<TArgsDef extends z.ZodObject<z.ZodRawShape> ? z.infer<TArgsDef> : InferArgs<TArgsDef & ArgsDef>, TConfig, InferVariables<TMiddleware>>;
318
383
  }, "Command">;
319
384
  /**
320
385
  * A map of command name to resolved {@link Command}. Used for subcommands and the manifest.
@@ -378,5 +443,5 @@ interface CliOptions<TSchema extends z.ZodType = z.ZodType> {
378
443
  commands?: string | CommandMap | Promise<CommandMap>;
379
444
  }
380
445
  //#endregion
381
- export { CommandDef as a, MiddlewareFn as c, Command as i, Context as l, AutoloadOptions as n, CommandMap as o, CliOptions as r, Middleware as s, ArgsDef as t };
382
- //# sourceMappingURL=types-kjpRau0U.d.ts.map
446
+ export { CommandDef as a, MiddlewareEnv as c, Command as i, MiddlewareFn as l, AutoloadOptions as n, CommandMap as o, CliOptions as r, Middleware as s, ArgsDef as t, Context as u };
447
+ //# sourceMappingURL=types-BaZ5WqVM.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-BaZ5WqVM.d.ts","names":[],"sources":["../src/context/types.ts","../src/types.ts"],"mappings":"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"}
@@ -20,4 +20,4 @@ interface ResolvePathOptions {
20
20
  }
21
21
  //#endregion
22
22
  export { ProjectRoot as n, ResolvePathOptions as r, PathSource as t };
23
- //# sourceMappingURL=types-Cz9h927W.d.ts.map
23
+ //# sourceMappingURL=types-C0CYivzY.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-Cz9h927W.d.ts","names":[],"sources":["../src/lib/project/types.ts"],"mappings":";;AAGA;;UAAiB,WAAA;EAAA,SACN,WAAA;EAAA,SACA,IAAA;AAAA;;;;KAMC,UAAA;AAKZ;;;AAAA,UAAiB,kBAAA;EAAA,SACN,OAAA;EAAA,SACA,MAAA,GAAS,UAAA;EAAA,SACT,QAAA;AAAA"}
1
+ {"version":3,"file":"types-C0CYivzY.d.ts","names":[],"sources":["../src/lib/project/types.ts"],"mappings":";;AAGA;;UAAiB,WAAA;EAAA,SACN,WAAA;EAAA,SACA,IAAA;AAAA;;;;KAMC,UAAA;AAKZ;;;AAAA,UAAiB,kBAAA;EAAA,SACN,OAAA;EAAA,SACA,MAAA,GAAS,UAAA;EAAA,SACT,QAAA;AAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kidd-cli/core",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "An opinionated CLI framework for Node.js",
5
5
  "keywords": [
6
6
  "cli",
@@ -25,18 +25,10 @@
25
25
  "types": "./dist/index.d.ts",
26
26
  "default": "./dist/index.js"
27
27
  },
28
- "./prompts": {
29
- "types": "./dist/lib/prompts.d.ts",
30
- "default": "./dist/lib/prompts.js"
31
- },
32
28
  "./logger": {
33
29
  "types": "./dist/lib/logger.d.ts",
34
30
  "default": "./dist/lib/logger.js"
35
31
  },
36
- "./output": {
37
- "types": "./dist/lib/output.d.ts",
38
- "default": "./dist/lib/output.js"
39
- },
40
32
  "./config": {
41
33
  "types": "./dist/lib/config.d.ts",
42
34
  "default": "./dist/lib/config.js"
@@ -64,14 +56,14 @@
64
56
  "dotenv": "^17.3.1",
65
57
  "es-toolkit": "^1.45.0",
66
58
  "jsonc-parser": "^3.3.1",
67
- "liquidjs": "^10.24.0",
59
+ "liquidjs": "^10.25.0",
68
60
  "picocolors": "^1.1.1",
69
61
  "ts-pattern": "^5.9.0",
70
62
  "yaml": "^2.8.2",
71
63
  "yargs": "^18.0.0",
72
64
  "zod": "^4.3.6",
73
- "@kidd-cli/config": "0.1.1",
74
- "@kidd-cli/utils": "0.1.1"
65
+ "@kidd-cli/utils": "0.1.2",
66
+ "@kidd-cli/config": "0.1.2"
75
67
  },
76
68
  "devDependencies": {
77
69
  "@types/node": "^25.3.3",
@@ -94,6 +86,7 @@
94
86
  "lint": "oxlint --ignore-pattern node_modules",
95
87
  "lint:fix": "oxlint --fix --ignore-pattern node_modules",
96
88
  "test": "vitest run",
89
+ "test:coverage": "vitest run --coverage",
97
90
  "test:watch": "vitest"
98
91
  }
99
92
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"config-BvGapuFJ.js","names":["parseJsonc","parseYaml","stringifyYaml"],"sources":["../src/utils/constants.ts","../src/lib/config/constants.ts","../src/lib/config/find.ts","../src/lib/config/parse.ts","../src/lib/config/create-config.ts"],"sourcesContent":["/**\n * Standard JSON indentation level used across the package.\n */\nexport const JSON_INDENT = 2\n\n/**\n * Default process exit code for error conditions.\n */\nexport const DEFAULT_EXIT_CODE = 1\n","/**\n * Supported configuration file formats.\n */\nexport type ConfigFormat = 'json' | 'jsonc' | 'yaml'\n\nexport { JSON_INDENT } from '@/utils/constants.js'\nexport const EMPTY_LENGTH = 0\nexport const CONFIG_EXTENSIONS = ['.jsonc', '.json', '.yaml'] as const\n","import { access } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nimport { attemptAsync } from '@kidd-cli/utils/fp'\n\nimport { findProjectRoot } from '@/lib/project/index.js'\n\nimport { CONFIG_EXTENSIONS } from './constants.js'\n\n/**\n * Generate the list of config file names to search for based on the CLI name.\n *\n * Produces names like `.myapp.jsonc`, `.myapp.json`, `.myapp.yaml` from the\n * supported extension list.\n *\n * @param name - The CLI name used to derive config file names.\n * @returns An array of config file names to search for.\n */\nexport function getConfigFileNames(name: string): string[] {\n return CONFIG_EXTENSIONS.map((ext) => `.${name}${ext}`)\n}\n\n/**\n * Check whether a file exists at the given path.\n *\n * @param filePath - The absolute file path to check.\n * @returns True when the file exists and is accessible.\n */\nexport async function fileExists(filePath: string): Promise<boolean> {\n const [error] = await attemptAsync(() => access(filePath))\n return !error\n}\n\n/**\n * Search a single directory for the first matching config file name.\n *\n * Checks each candidate file name in order and returns the path of the first\n * one that exists on disk.\n *\n * @param dir - The directory to search in.\n * @param fileNames - Candidate config file names to look for.\n * @returns The full path to the first matching config file, or null if none found.\n */\nexport async function findConfigFile(\n dir: string,\n fileNames: readonly string[]\n): Promise<string | null> {\n const results = await Promise.all(\n fileNames.map(async (fileName) => {\n const filePath = join(dir, fileName)\n const exists = await fileExists(filePath)\n if (exists) {\n return filePath\n }\n return null\n })\n )\n const found = results.find((result): result is string => result !== null)\n return found ?? null\n}\n\n/**\n * Search for a config file across multiple directories.\n *\n * Searches in order: explicit search paths, the current working directory,\n * and the project root (if different from cwd). Returns the path of the\n * first matching file found.\n *\n * @param options - Search options including cwd, file names, and optional search paths.\n * @returns The full path to the config file, or null if not found.\n */\nexport async function findConfig(options: {\n cwd: string\n fileNames: string[]\n searchPaths?: string[]\n}): Promise<string | null> {\n const { fileNames, cwd, searchPaths } = options\n\n if (searchPaths) {\n const searchResults = await Promise.all(\n searchPaths.map((dir) => findConfigFile(dir, fileNames))\n )\n const found = searchResults.find((result): result is string => result !== null)\n if (found) {\n return found\n }\n }\n\n const fromCwd = await findConfigFile(cwd, fileNames)\n if (fromCwd) {\n return fromCwd\n }\n\n const projectRoot = findProjectRoot(cwd)\n if (projectRoot && projectRoot.path !== cwd) {\n const fromRoot = await findConfigFile(projectRoot.path, fileNames)\n if (fromRoot) {\n return fromRoot\n }\n }\n\n return null\n}\n","import { extname } from 'node:path'\n\nimport { attempt, err, match } from '@kidd-cli/utils/fp'\nimport { jsonParse, jsonStringify } from '@kidd-cli/utils/json'\nimport type { ParseError } from 'jsonc-parser'\nimport { parse as parseJsonc, printParseErrorCode } from 'jsonc-parser'\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml'\n\nimport type { ConfigFormat } from './constants.js'\nimport { EMPTY_LENGTH } from './constants.js'\nimport type { ConfigOperationResult } from './types.js'\n\n/**\n * Determine the config format from a file path's extension.\n *\n * @param filePath - The file path to inspect.\n * @returns The detected config format ('json', 'jsonc', or 'yaml').\n */\nexport function getFormat(filePath: string): ConfigFormat {\n const ext = extname(filePath)\n return match(ext)\n .with('.jsonc', () => 'jsonc' as const)\n .with('.yaml', () => 'yaml' as const)\n .otherwise(() => 'json' as const)\n}\n\n/**\n * Parse a JSON string and return the result as a ConfigOperationResult.\n *\n * @param content - The raw JSON string to parse.\n * @param filePath - The file path used in error messages.\n * @returns A ConfigOperationResult with the parsed data or a parse error.\n */\nexport function parseJson(content: string, filePath: string): ConfigOperationResult<unknown> {\n const [error, result] = jsonParse(content)\n if (error) {\n return err(`Failed to parse JSON in ${filePath}: ${error.message}`)\n }\n return [null, result]\n}\n\n/**\n * Parse a JSONC (JSON with comments) string and return the result as a ConfigOperationResult.\n *\n * @param content - The raw JSONC string to parse.\n * @param filePath - The file path used in error messages.\n * @returns A ConfigOperationResult with the parsed data or a parse error.\n */\nexport function parseJsoncContent(\n content: string,\n filePath: string\n): ConfigOperationResult<unknown> {\n const errors: ParseError[] = []\n const result = parseJsonc(content, errors, {\n allowEmptyContent: false,\n allowTrailingComma: true,\n })\n if (errors.length > EMPTY_LENGTH) {\n const errorMessages = errors\n .map(\n (parseError) =>\n ` - ${printParseErrorCode(parseError.error)} at offset ${parseError.offset}`\n )\n .join('\\n')\n return err(`Failed to parse JSONC in ${filePath}:\\n${errorMessages}`)\n }\n return [null, result]\n}\n\n/**\n * Parse a YAML string and return the result as a ConfigOperationResult.\n *\n * @param content - The raw YAML string to parse.\n * @param filePath - The file path used in error messages.\n * @returns A ConfigOperationResult with the parsed data or a parse error.\n */\nexport function parseYamlContent(\n content: string,\n filePath: string\n): ConfigOperationResult<unknown> {\n const [error, result] = attempt(() => parseYaml(content))\n if (error) {\n return err(`Failed to parse YAML in ${filePath}: ${String(error)}`)\n }\n return [null, result]\n}\n\n/**\n * Options for parsing config file content.\n */\nexport interface ParseContentOptions {\n readonly content: string\n readonly filePath: string\n readonly format: ConfigFormat\n}\n\n/**\n * Parse config file content using the appropriate parser for the given format.\n *\n * @param options - Parse content options.\n * @returns A ConfigOperationResult with the parsed data or an error.\n */\nexport function parseContent(options: ParseContentOptions): ConfigOperationResult<unknown> {\n const { content, filePath, format } = options\n return match(format)\n .with('json', () => parseJson(content, filePath))\n .with('jsonc', () => parseJsoncContent(content, filePath))\n .with('yaml', () => parseYamlContent(content, filePath))\n .exhaustive()\n}\n\n/**\n * Serialize data to a string in the specified config format.\n *\n * @param data - The data to serialize.\n * @param format - The target config format.\n * @returns The serialized string representation.\n */\nexport function serializeContent(data: unknown, format: ConfigFormat): string {\n return match(format)\n .with('json', () => {\n const [, json] = jsonStringify(data, { pretty: true })\n return `${json}\\n`\n })\n .with('jsonc', () => {\n const [, json] = jsonStringify(data, { pretty: true })\n return `${json}\\n`\n })\n .with('yaml', () => stringifyYaml(data))\n .exhaustive()\n}\n\n/**\n * Get the file extension string for a given config format.\n *\n * @param format - The config format.\n * @returns The file extension including the leading dot (e.g. '.json').\n */\nexport function getExtension(format: ConfigFormat): string {\n return match(format)\n .with('json', () => '.json')\n .with('jsonc', () => '.jsonc')\n .with('yaml', () => '.yaml')\n .exhaustive()\n}\n","import { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\n\nimport { attemptAsync, err, match } from '@kidd-cli/utils/fp'\nimport { formatZodIssues } from '@kidd-cli/utils/validate'\nimport type { ZodTypeAny, output } from 'zod'\n\nimport { findConfig, getConfigFileNames } from './find.js'\nimport { getExtension, getFormat, parseContent, serializeContent } from './parse.js'\nimport type {\n Config,\n ConfigOperationResult,\n ConfigOptions,\n ConfigResult,\n ConfigWriteOptions,\n ConfigWriteResult,\n} from './types.js'\n\n/**\n * Create a typed config client that loads, validates, and writes config files.\n *\n * @param options - Config client options including name and Zod schema.\n * @returns A {@link Config} client instance.\n */\nexport function createConfigClient<TSchema extends ZodTypeAny>(\n options: ConfigOptions<TSchema>\n): Config<output<TSchema>> {\n const { name, schema, searchPaths } = options\n const fileNames = getConfigFileNames(name)\n\n /**\n * Find a config file in the given directory.\n *\n * @private\n * @param cwd - Working directory to search from.\n * @returns The path to the config file, or null if not found.\n */\n async function find(cwd?: string): Promise<string | null> {\n return findConfig({\n cwd: cwd ?? process.cwd(),\n fileNames,\n searchPaths,\n })\n }\n\n /**\n * Load and validate a config file.\n *\n * @private\n * @param cwd - Working directory to search from.\n * @returns A ConfigOperationResult with the loaded config, or [null, null] if not found.\n */\n async function load(\n cwd?: string\n ): Promise<ConfigOperationResult<ConfigResult<output<TSchema>>> | readonly [null, null]> {\n const filePath = await find(cwd)\n if (!filePath) {\n return [null, null]\n }\n\n const [readError, content] = await attemptAsync(() => readFile(filePath, 'utf8'))\n if (readError || content === null) {\n const errorDetail = resolveReadErrorDetail(readError)\n return err(`Failed to read config at ${filePath}: ${errorDetail}`)\n }\n\n const format = getFormat(filePath)\n const parsedResult = parseContent({ content, filePath, format })\n\n if (parsedResult[0]) {\n return [parsedResult[0], null]\n }\n\n const result = schema.safeParse(parsedResult[1])\n if (!result.success) {\n const { message } = formatZodIssues(result.error.issues, '\\n')\n return err(`Invalid config in ${filePath}:\\n${message}`)\n }\n\n return [\n null,\n {\n config: result.data,\n filePath,\n format,\n },\n ]\n }\n\n /**\n * Validate and write config data to a file.\n *\n * @private\n * @param data - The config data to write.\n * @param writeOptions - Write options including path and format.\n * @returns A ConfigOperationResult with the write result.\n */\n async function write(\n data: output<TSchema>,\n writeOptions: ConfigWriteOptions = {}\n ): Promise<ConfigOperationResult<ConfigWriteResult>> {\n const result = schema.safeParse(data)\n if (!result.success) {\n const { message } = formatZodIssues(result.error.issues, '\\n')\n return err(`Invalid config data:\\n${message}`)\n }\n\n const resolvedFormat = match(writeOptions)\n .when(\n (opts) => opts.format !== null && opts.format !== undefined,\n (opts) => opts.format ?? ('jsonc' as const)\n )\n .when(\n (opts) => opts.filePath !== null && opts.filePath !== undefined,\n (opts) => getFormat(opts.filePath ?? '')\n )\n .otherwise(() => 'jsonc' as const)\n\n const resolvedFilePath = match(writeOptions.filePath)\n .when(\n (fp) => fp !== null && fp !== undefined,\n (fp) => fp ?? ''\n )\n .otherwise(() => {\n const dir = writeOptions.dir ?? process.cwd()\n const ext = getExtension(resolvedFormat)\n return join(dir, `.${name}${ext}`)\n })\n\n const serialized = serializeContent(result.data, resolvedFormat)\n\n const [mkdirError] = await attemptAsync(() =>\n mkdir(dirname(resolvedFilePath), { recursive: true })\n )\n if (mkdirError) {\n return err(`Failed to create directory for ${resolvedFilePath}: ${String(mkdirError)}`)\n }\n\n const [writeError] = await attemptAsync(() => writeFile(resolvedFilePath, serialized, 'utf8'))\n if (writeError) {\n return err(`Failed to write config to ${resolvedFilePath}: ${String(writeError)}`)\n }\n\n return [null, { filePath: resolvedFilePath, format: resolvedFormat }]\n }\n\n return { find, load, write }\n}\n\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the error detail string from a read error.\n *\n * @private\n * @param readError - The error from the read operation, or null.\n * @returns A descriptive error string.\n */\nfunction resolveReadErrorDetail(readError: unknown): string {\n if (readError) {\n return String(readError)\n }\n return 'empty file'\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"create-store-BQUX0tAn.js","names":[],"sources":["../src/lib/store/create-store.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, 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 if (!existsSync(filePath)) {\n return null\n }\n const [error, content] = attempt(() => readFileSync(filePath, 'utf8'))\n if (error) {\n return null\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 return {\n getFilePath,\n getGlobalDir,\n getLocalDir,\n load,\n loadRaw,\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,62 +0,0 @@
1
- import { Result } from "@kidd-cli/utils/fp";
2
- import { Liquid } from "liquidjs";
3
-
4
- //#region src/lib/output/types.d.ts
5
- /**
6
- * Options for JSON serialization output.
7
- */
8
- interface JsonOutputOptions {
9
- pretty?: boolean;
10
- redact?: boolean;
11
- }
12
- /**
13
- * Parameters for writing content to a file.
14
- */
15
- interface WriteParams {
16
- path: string;
17
- content: string;
18
- }
19
- /**
20
- * Parameters for rendering data to markdown via a Liquid template.
21
- */
22
- interface ToMarkdownParams {
23
- data: unknown;
24
- type: string;
25
- }
26
- /**
27
- * Options for creating an output instance.
28
- */
29
- interface CreateOutputOptions {
30
- output?: NodeJS.WriteStream;
31
- templates?: string;
32
- filters?: Record<string, (...args: unknown[]) => unknown>;
33
- context?: (params: ToMarkdownParams) => Record<string, unknown>;
34
- }
35
- /**
36
- * Structured output utilities for JSON, markdown, and file writing.
37
- */
38
- interface CliOutput {
39
- toJson(data: unknown, options?: JsonOutputOptions): string;
40
- toMarkdown(params: ToMarkdownParams): Result<string, Error>;
41
- json(data: unknown, options?: JsonOutputOptions): void;
42
- write(params: WriteParams): Result<void, Error>;
43
- print(content: string): void;
44
- }
45
- //#endregion
46
- //#region src/lib/output/create-output.d.ts
47
- /**
48
- * Create a new {@link CliOutput} instance for structured CLI output.
49
- *
50
- * @param options - Output configuration.
51
- * @returns A CliOutput instance.
52
- */
53
- declare function createOutput(options?: CreateOutputOptions): CliOutput;
54
- //#endregion
55
- //#region src/lib/output/defaults.d.ts
56
- /**
57
- * Default output instance writing to stdout.
58
- */
59
- declare const output: CliOutput;
60
- //#endregion
61
- export { type CliOutput, type CreateOutputOptions, type JsonOutputOptions, type ToMarkdownParams, type WriteParams, createOutput, output };
62
- //# sourceMappingURL=output.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"output.d.ts","names":[],"sources":["../../src/lib/output/types.ts","../../src/lib/output/create-output.ts","../../src/lib/output/defaults.ts"],"mappings":";;;;;;AAMA;UAAiB,iBAAA;EACf,MAAA;EACA,MAAA;AAAA;AAMF;;;AAAA,UAAiB,WAAA;EACf,IAAA;EACA,OAAA;AAAA;;;;UAMe,gBAAA;EACf,IAAA;EACA,IAAA;AAAA;;;;UAMe,mBAAA;EACf,MAAA,GAAS,MAAA,CAAO,WAAA;EAChB,SAAA;EACA,OAAA,GAAU,MAAA,aAAmB,IAAA;EAC7B,OAAA,IAAW,MAAA,EAAQ,gBAAA,KAAqB,MAAA;AAAA;;;;UAMzB,SAAA;EACf,MAAA,CAAO,IAAA,WAAe,OAAA,GAAU,iBAAA;EAChC,UAAA,CAAW,MAAA,EAAQ,gBAAA,GAAmB,MAAA,SAAe,KAAA;EACrD,IAAA,CAAK,IAAA,WAAe,OAAA,GAAU,iBAAA;EAC9B,KAAA,CAAM,MAAA,EAAQ,WAAA,GAAc,MAAA,OAAa,KAAA;EACzC,KAAA,CAAM,OAAA;AAAA;;;;;;AAvCR;;;iBCegB,YAAA,CAAa,OAAA,GAAS,mBAAA,GAA2B,SAAA;;;;;;cCfpD,MAAA,EAAQ,SAAA"}