{"version":3,"file":"config.mjs","names":[],"sources":["../../src/extension/config.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec';\nimport { applyValues } from '../core/args.ts';\nimport { ConfigError } from '../core/errors.ts';\nimport { defineInterceptor } from '../core/interceptors.ts';\nimport { thenMaybe } from '../core/results.ts';\nimport type { AnyPadroneBuilder, CommandTypesBase, InterceptorValidateContext } from '../types/index.ts';\nimport type { WithAsync } from '../util/type-utils.ts';\nimport { getRootCommand } from '../util/utils.ts';\n\n// ── Types ────────────────────────────────────────────────────────────────\n\nexport type PadroneConfigOptions = {\n  /** Config file names to auto-detect (e.g. `['config.json', '.myapprc']`). First found is used. */\n  files?: string | string[];\n  /** Schema to validate and transform config file data into the args shape. */\n  schema?: StandardSchemaV1;\n  /** Disable this extension. */\n  disabled?: boolean;\n  /** Whether to add `--config` / `-c` flag support. Defaults to `true`. */\n  flag?: boolean;\n  /** Whether subcommands inherit this interceptor. Defaults to `true`. */\n  inherit?: boolean;\n  /**\n   * Search for config files in the user's platform-specific config directory.\n   * - `true` — use the program name as the subdirectory (e.g. program `'myapp'` → `~/.config/myapp/`).\n   * - `string` — use a custom app name as the subdirectory.\n   * - `false` — disable (default).\n   *\n   * Directories searched (after cwd):\n   * - **Linux**: `$XDG_CONFIG_HOME/<app>` or `~/.config/<app>`\n   * - **macOS**: `~/Library/Application Support/<app>` (or `$XDG_CONFIG_HOME/<app>` when set)\n   * - **Windows**: `%APPDATA%\\<app>`\n   *\n   * Config files found in cwd always take precedence over XDG paths.\n   */\n  xdg?: string | boolean;\n  /**\n   * Custom config loader. When provided, replaces the built-in file system loader.\n   * Useful for testing or non-CLI environments.\n   */\n  loadConfig?: (\n    files: string | string[],\n    xdgAppName?: string,\n  ) => Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>;\n};\n\n// ── File system config loader ───────────────────────────────────────────\n\n// Lazily resolved Node.js modules — cached after first import to keep loadConfig sync after initialization.\nlet _fs: typeof import('node:fs') | undefined;\nlet _path: typeof import('node:path') | undefined;\n\nasync function initNodeModules(): Promise<void> {\n  if (_fs && _path) return;\n  _fs = await import('node:fs');\n  _path = await import('node:path');\n}\n\n// Eagerly start caching node modules so loadConfig is sync by the time it's called.\ntry {\n  if (typeof process !== 'undefined') initNodeModules();\n} catch {\n  // Non-CLI environments (browser, edge) — ignore\n}\n\nfunction getUserConfigDir(path: typeof import('node:path'), appName: string): string | undefined {\n  const platform = process.platform;\n\n  // Respect XDG_CONFIG_HOME on all platforms when explicitly set\n  const xdgHome = process.env.XDG_CONFIG_HOME;\n  if (xdgHome) return path.join(xdgHome, appName);\n\n  const home = process.env.HOME || process.env.USERPROFILE;\n  if (!home) return undefined;\n\n  if (platform === 'win32') {\n    const appData = process.env.APPDATA;\n    return appData ? path.join(appData, appName) : path.join(home, 'AppData', 'Roaming', appName);\n  }\n  if (platform === 'darwin') return path.join(home, 'Library', 'Application Support', appName);\n\n  // Linux and other Unix — default XDG path\n  return path.join(home, '.config', appName);\n}\n\nfunction resolveConfigPath(fs: any, path: any, cwd: string, files: string | string[], xdgAppName?: string): string | undefined {\n  if (typeof files === 'string') {\n    const abs = path.isAbsolute(files) ? files : path.resolve(cwd, files);\n    if (!fs.existsSync(abs)) {\n      console.error(`Config file not found: ${abs}`);\n      return undefined;\n    }\n    return abs;\n  }\n\n  // Search in cwd first\n  for (const candidate of files) {\n    const abs = path.isAbsolute(candidate) ? candidate : path.resolve(cwd, candidate);\n    if (fs.existsSync(abs)) return abs;\n  }\n\n  // Then search in the user config directory (XDG / platform-specific)\n  if (xdgAppName) {\n    const configDir = getUserConfigDir(path, xdgAppName);\n    if (configDir) {\n      for (const candidate of files) {\n        const abs = path.join(configDir, candidate);\n        if (fs.existsSync(abs)) return abs;\n      }\n    }\n  }\n\n  return undefined;\n}\n\nfunction loadConfigSync(\n  fs: typeof import('node:fs'),\n  path: typeof import('node:path'),\n  files: string | string[],\n  xdgAppName?: string,\n): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined> {\n  const cwd = process.cwd();\n  const absolutePath = resolveConfigPath(fs, path, cwd, files, xdgAppName);\n  if (!absolutePath) return undefined;\n\n  const getContent = () => fs.readFileSync(absolutePath, 'utf-8');\n  const ext = path.extname(absolutePath).toLowerCase();\n\n  if (ext === '.yaml' || ext === '.yml') return Bun.YAML.parse(getContent()) as any;\n  if (ext === '.toml') return Bun.TOML.parse(getContent()) as any;\n  if (ext === '.jsonc') return Bun.JSONC.parse(getContent()) as any;\n  if (ext === '.json') {\n    if (Bun.JSONC) return Bun.JSONC.parse(getContent()) as any;\n    try {\n      return JSON.parse(getContent());\n    } catch {\n      return Bun.JSONC.parse(getContent()) as any;\n    }\n  }\n  if (ext === '.js' || ext === '.cjs' || ext === '.mjs' || ext === '.ts' || ext === '.cts' || ext === '.mts') {\n    return import(/* @vite-ignore */ absolutePath).then((mod) => mod.default ?? mod);\n  }\n\n  // Unknown extension — try JSON\n  try {\n    return JSON.parse(getContent());\n  } catch {\n    console.error(`Unable to parse config file: ${absolutePath}`);\n    return undefined;\n  }\n}\n\n/**\n * Built-in config file loader. Directly accesses the file system.\n * Returns `undefined` in non-CLI environments where `node:fs` is unavailable.\n */\nfunction loadConfig(\n  files: string | string[],\n  xdgAppName?: string,\n): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined> {\n  if (typeof process === 'undefined') return undefined;\n\n  try {\n    if (_fs && _path) return loadConfigSync(_fs, _path, files, xdgAppName);\n    return initNodeModules().then(() => loadConfigSync(_fs!, _path!, files, xdgAppName));\n  } catch {\n    return undefined;\n  }\n}\n\n// ── Extension ────────────────────────────────────────────────────────────\n\n/**\n * Extension that handles config file loading, validation, and merging into command arguments.\n *\n * Features:\n * - `--config` / `-c` flag for explicit config file path (can be disabled via `flag: false`)\n * - Auto-detection of config files from a list of candidate names\n * - Optional schema validation and transformation of config data\n * - Directly accesses the file system (gracefully no-ops in non-CLI environments)\n *\n * Config values have the lowest precedence (CLI > stdin > env > config).\n *\n * Not included in the default built-in extensions — must be explicitly added:\n * ```ts\n * createPadrone('my-cli')\n *   .extend(padroneConfig({\n *     files: ['config.json', '.myapprc'],\n *     schema: z.object({ port: z.number(), host: z.string() }),\n *   }))\n * ```\n */\nexport function padroneConfig(options?: PadroneConfigOptions): <T extends CommandTypesBase>(builder: T) => WithAsync<T> {\n  if (options?.disabled) {\n    const disabled = defineInterceptor({ id: 'padrone:config', name: 'padrone:config', order: -999, disabled: true }, () => ({}));\n    return ((builder: AnyPadroneBuilder) => builder.intercept(disabled)) as any;\n  }\n\n  const configFiles = options?.files ? (Array.isArray(options.files) ? options.files : [options.files]) : undefined;\n  const configSchema = options?.schema;\n  const flagEnabled = options?.flag !== false;\n  const inherit = options?.inherit;\n  const xdgOption = options?.xdg;\n  const configLoader = options?.loadConfig ?? loadConfig;\n\n  const interceptor = defineInterceptor(\n    { id: 'padrone:config', name: 'padrone:config', order: -999, ...(inherit === false && { inherit: false }) },\n    () => ({\n      validate(ctx: InterceptorValidateContext, next) {\n        // Extract --config / -c from rawArgs\n        let explicitConfigPath: string | undefined;\n        if (flagEnabled) {\n          explicitConfigPath = (ctx.rawArgs.config ?? ctx.rawArgs.c) as string | undefined;\n          if (typeof explicitConfigPath === 'string') {\n            delete ctx.rawArgs.config;\n            delete ctx.rawArgs.c;\n          }\n        }\n\n        // Skip entirely when there's nothing to load\n        if (!explicitConfigPath && !configFiles) return next();\n\n        // Resolve XDG app name: true → derive from root command name, string → use as-is\n        let xdgAppName: string | undefined;\n        if (typeof xdgOption === 'string') xdgAppName = xdgOption;\n        else if (xdgOption === true) xdgAppName = getRootCommand(ctx.command).name;\n\n        // Load config data: explicit --config flag takes priority, then auto-detect\n        const configDataOrPromise = configLoader(explicitConfigPath ?? configFiles ?? [], xdgAppName);\n\n        const applyConfig = (configData: Record<string, unknown> | undefined) => {\n          if (!configData) return next();\n\n          // Validate against schema if provided\n          if (configSchema) {\n            const validated = configSchema['~standard'].validate(configData);\n            return thenMaybe(validated, (result) => {\n              if (result.issues) {\n                const issueMessages = result.issues\n                  .map((i: StandardSchemaV1.Issue) => `  - ${i.path?.join('.') || 'root'}: ${i.message}`)\n                  .join('\\n');\n                throw new ConfigError(`Invalid config file:\\n${issueMessages}`, {\n                  command: ctx.command.path || ctx.command.name,\n                });\n              }\n              const validatedData = result.value as Record<string, unknown>;\n              const mergedRawArgs = applyValues(ctx.rawArgs, validatedData);\n              return next({ rawArgs: mergedRawArgs });\n            });\n          }\n\n          // No schema — pass through as-is\n          const mergedRawArgs = applyValues(ctx.rawArgs, configData);\n          return next({ rawArgs: mergedRawArgs });\n        };\n\n        return thenMaybe(configDataOrPromise, applyConfig);\n      },\n    }),\n  );\n\n  return ((builder: AnyPadroneBuilder) => builder.intercept(interceptor)) as any;\n}\n"],"mappings":"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"}