envprism 0.3.0 → 0.3.1
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/README.md +46 -43
- package/dist/bin/envprism.mjs +254 -262
- package/dist/bin/envprism.mjs.map +1 -1
- package/dist/chunks/app-Bp7QXiLx.mjs +1965 -0
- package/dist/chunks/app-Bp7QXiLx.mjs.map +1 -0
- package/dist/chunks/matrix-CG1Msljr.mjs +740 -0
- package/dist/chunks/matrix-CG1Msljr.mjs.map +1 -0
- package/dist/chunks/serialize-DWF9VxrS.mjs +59 -0
- package/dist/chunks/serialize-DWF9VxrS.mjs.map +1 -0
- package/dist/config/define.mjs +14 -5
- package/dist/config/define.mjs.map +1 -1
- package/dist/index.mjs +3 -18
- package/package.json +16 -16
- package/dist/chunks/app-DgjyG_HE.mjs +0 -1800
- package/dist/chunks/app-DgjyG_HE.mjs.map +0 -1
- package/dist/chunks/matrix-C1pErf9Z.mjs +0 -676
- package/dist/chunks/matrix-C1pErf9Z.mjs.map +0 -1
- package/dist/chunks/serialize-D_ZNIgRa.mjs +0 -41
- package/dist/chunks/serialize-D_ZNIgRa.mjs.map +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matrix-CG1Msljr.mjs","names":[],"sources":["../../src/core/mask.ts","../../src/config/schema.ts","../../src/tui/format.ts","../../src/config/resolve.ts","../../src/config/load.ts","../../src/core/base.ts","../../src/core/diff.ts","../../src/core/parse.ts","../../src/core/discover.ts","../../src/core/sections.ts","../../src/core/matrix.ts"],"sourcesContent":["const SECRET_TOKENS = [\n 'SECRET',\n 'TOKEN',\n 'PASSWORD',\n 'PASSWD',\n 'PWD',\n 'KEY',\n 'PRIVATE',\n 'CREDENTIAL',\n 'AUTH',\n 'DSN'\n] as const;\n\n/**\n * Heuristic: should a value with this key be masked by default? Matches any\n * underscore-separated segment of {@link SECRET_TOKENS} (case-insensitive),\n * with carve-outs for false positives like `*_PUBLIC_KEY`, `PUBLIC_*`, and\n * keys whose final segment is `ID` (e.g. `API_KEY_ID` is an identifier, not\n * a secret).\n */\nexport function isSecretKey(\n key: string,\n tokens: readonly string[] = SECRET_TOKENS\n): boolean {\n const upper = key.toUpperCase();\n const segments = upper.split('_').filter(Boolean);\n if (segments.length === 0) return false;\n if (segments[segments.length - 1] === 'ID') return false;\n if (segments[0] === 'PUBLIC') return false;\n if (segments.includes('PUBLIC')) return false;\n\n return segments.some((seg) =>\n tokens.some((token) => seg === token || seg.endsWith(token))\n );\n}\n\n/**\n * Render a masked placeholder that hints at the original value's length\n * without leaking content. Returns `••••` when the input is empty.\n */\nexport function maskValue(value: string): string {\n if (value.length === 0) return '••••';\n const dots = '•'.repeat(Math.min(value.length, 8));\n return value.length > 8 ? `${dots} (${value.length})` : dots;\n}\n","// Config schema for envprism. Kept RGBA-free (no @opentui/core import) so the\n// public `envprism/config` entry and the Node-safe `diff` path never pull in\n// the TUI runtime. The resolved RGBA theme lives in src/tui/theme.ts.\n\nexport type GroupingMode = 'auto' | 'banner' | 'prefix';\n\nexport interface DiscoveryConfig {\n /** Paths scanned when none are passed on the CLI. */\n paths: string[];\n /** Filename suffixes to skip (editor/backup files). Replaces the default. */\n skipSuffixes: string[];\n /** Appended to {@link skipSuffixes} (or its default). */\n skipSuffixesExtra: string[];\n /** Sort `.env.example` first in the discovered file order. */\n exampleFirst: boolean;\n}\n\nexport interface BaseConfig {\n /** Filename treated as the default base when present. */\n name: string;\n /** Ordered basenames tried before falling back to the first file. */\n priority: string[];\n}\n\nexport interface HeuristicsConfig {\n /** Secret-key tokens (case-insensitive). Replaces the built-in list. */\n secretTokens: string[];\n /** Appended to {@link secretTokens} (or its default). */\n secretTokensExtra: string[];\n /** Placeholder regex alternation atoms. Replaces the built-in list. */\n placeholders: string[];\n /** Appended to {@link placeholders} (or its default). */\n placeholdersExtra: string[];\n /** Default TUI grouping. 'auto' picks banner when the base has banners. */\n grouping: GroupingMode;\n}\n\nexport interface DiffConfig {\n /** Emit JSON instead of the text table by default. */\n json: boolean;\n /** Exit code used by `--check` when files drift. */\n checkExitCode: number;\n}\n\n/** Partial hex (`#rrggbb`) overrides for the TUI palette; gaps use defaults. */\nexport interface ThemeConfig {\n fg?: string;\n fgDim?: string;\n fgHeader?: string;\n fgBase?: string;\n fgSection?: string;\n differs?: string;\n extra?: string;\n placeholder?: string;\n modified?: string;\n fgDirty?: string;\n missing?: string;\n focusBg?: string;\n}\n\nexport type ThemeKey = keyof ThemeConfig;\n\nexport interface LayoutConfig {\n keyColWidth: number;\n valueColMin: number;\n sidebarWidth: number;\n rowGap: number;\n cellPadX: number;\n}\n\nexport interface TuiConfig {\n theme: ThemeConfig;\n layout: LayoutConfig;\n undoLimit: number;\n /** Start with secret-suspect values masked (toggle in-app with Ctrl-T). */\n maskSecrets: boolean;\n}\n\n/** Fully-resolved, internal config shape (every field populated). */\nexport interface EnvprismConfig {\n discovery: DiscoveryConfig;\n base: BaseConfig;\n heuristics: HeuristicsConfig;\n diff: DiffConfig;\n tui: TuiConfig;\n}\n\n/** Deep-partial shape authored in envprism.config.{ts,js,mjs,json}. */\nexport interface EnvprismUserConfig {\n discovery?: Partial<DiscoveryConfig>;\n base?: Partial<BaseConfig>;\n heuristics?: Partial<HeuristicsConfig>;\n diff?: Partial<DiffConfig>;\n tui?: {\n theme?: ThemeConfig;\n layout?: Partial<LayoutConfig>;\n undoLimit?: number;\n maskSecrets?: boolean;\n };\n}\n\n/**\n * Built-in defaults — the single source of truth for every tunable value.\n * Mirrors the previously hardcoded constants (mask.ts SECRET_TOKENS,\n * format.ts PLACEHOLDER_RE atoms, discover.ts SKIP_SUFFIXES, theme.ts layout).\n */\nexport const DEFAULT_CONFIG: EnvprismConfig = {\n discovery: {\n paths: ['.'],\n skipSuffixes: ['.swp', '~', '.bak'],\n skipSuffixesExtra: [],\n exampleFirst: true\n },\n base: {\n name: '.env.example',\n priority: []\n },\n heuristics: {\n secretTokens: [\n 'SECRET',\n 'TOKEN',\n 'PASSWORD',\n 'PASSWD',\n 'PWD',\n 'KEY',\n 'PRIVATE',\n 'CREDENTIAL',\n 'AUTH',\n 'DSN'\n ],\n secretTokensExtra: [],\n placeholders: [\n 'todo',\n 'fixme',\n 'changeme',\n 'placeholder',\n 'tbd',\n 'x{3,}',\n 'your[_-]?(secret|key|token|password|api[_-]?key)(_here)?',\n 'replace[_-]?me'\n ],\n placeholdersExtra: [],\n grouping: 'auto'\n },\n diff: {\n json: false,\n checkExitCode: 1\n },\n tui: {\n theme: {},\n layout: {\n keyColWidth: 22,\n valueColMin: 18,\n sidebarWidth: 30,\n rowGap: 0,\n cellPadX: 1\n },\n undoLimit: 50,\n maskSecrets: true\n }\n};\n","import { maskValue } from '@/core/mask.ts';\nimport type { EnvFile, KvEntry } from '@/core/types.ts';\n\nexport const PLACEHOLDER_RE =\n /^(todo|fixme|changeme|placeholder|tbd|x{3,}|your[_-]?(secret|key|token|password|api[_-]?key)(_here)?|replace[_-]?me)$/i;\n\nexport function isPlaceholderValue(\n value: string,\n re: RegExp = PLACEHOLDER_RE\n): boolean {\n const v = value.trim();\n if (v.length === 0) return false;\n return re.test(v);\n}\n\nexport function formatValue(\n value: string | undefined,\n secret: boolean\n): string {\n if (value === undefined) return '';\n if (secret) return maskValue(value);\n return value;\n}\n\nexport function matchesFilter(key: string, filter: string): boolean {\n if (!filter) return true;\n return key.toLowerCase().includes(filter.toLowerCase());\n}\n\nexport function truncate(text: string, width: number): string {\n if (width <= 0) return '';\n if (text.length <= width) return text;\n if (width <= 1) return '…';\n return `${text.slice(0, width - 1)}…`;\n}\n\nexport function findKvEntry(file: EnvFile, key: string): KvEntry | undefined {\n for (const e of file.entries) {\n if (e.kind === 'kv' && e.key === key) return e;\n }\n return undefined;\n}\n","import consola from 'consola';\nimport { defu } from 'defu';\nimport { isSecretKey } from '@/core/mask.ts';\nimport {\n DEFAULT_CONFIG,\n type EnvprismConfig,\n type EnvprismUserConfig,\n type GroupingMode,\n type ThemeConfig,\n type ThemeKey\n} from '@/config/schema.ts';\nimport { isPlaceholderValue } from '@tui/format.ts';\n\n/** Hex source of truth for the TUI palette (resolved to RGBA in theme.ts). */\nexport const DEFAULT_THEME_HEX: Record<ThemeKey, string> = {\n fg: '#cccccc',\n fgDim: '#666666',\n fgHeader: '#ffffff',\n fgBase: '#82aaff',\n fgSection: '#82aaff',\n differs: '#ffd866',\n extra: '#ffd866',\n placeholder: '#ffd866',\n modified: '#7fce6a',\n fgDirty: '#7fce6a',\n missing: '#ff6b6b',\n focusBg: '#3a3f4b'\n};\n\nconst HEX_RE = /^#[0-9a-fA-F]{6}$/;\n\n/**\n * Merge user hex overrides onto {@link DEFAULT_THEME_HEX}. Invalid hex values\n * warn (light runtime guard) and fall back to the default. RGBA-free so it is\n * unit-testable under Node without loading the TUI runtime; theme.ts wraps the\n * result with RGBA.fromHex.\n */\nexport function resolveThemeHex(\n theme: ThemeConfig = {},\n warn: (msg: string) => void = consola.warn\n): Record<ThemeKey, string> {\n const out = { ...DEFAULT_THEME_HEX };\n for (const key of Object.keys(DEFAULT_THEME_HEX) as ThemeKey[]) {\n const override = theme[key];\n if (override === undefined) continue;\n if (HEX_RE.test(override)) {\n out[key] = override;\n } else {\n warn(`envprism: ignoring invalid hex for theme.${key}: \"${override}\"`);\n }\n }\n return out;\n}\n\n/**\n * Resolve a replace-or-extend list field. An explicit `replace` list wins over\n * the built-in `fallback`; `extra` is always appended to whatever the current\n * list is. De-duplicated so repeated entries are harmless.\n */\nfunction pickList(\n replace: string[] | undefined,\n extra: string[] | undefined,\n fallback: string[]\n): string[] {\n const base = replace ?? fallback;\n return [...new Set([...base, ...(extra ?? [])])];\n}\n\n/**\n * Merge a deep-partial user config onto {@link DEFAULT_CONFIG}. Scalars and\n * plain objects use defu (user wins, gaps fall back). List fields are handled\n * manually because defu concatenates arrays — wrong for our replace semantics:\n * `*` lists replace the default, `*Extra` lists append. The folded result puts\n * everything into the canonical field and leaves `*Extra` empty.\n */\nexport function mergeConfig(user: EnvprismUserConfig = {}): EnvprismConfig {\n return {\n discovery: {\n paths: user.discovery?.paths ?? DEFAULT_CONFIG.discovery.paths,\n skipSuffixes: pickList(\n user.discovery?.skipSuffixes,\n user.discovery?.skipSuffixesExtra,\n DEFAULT_CONFIG.discovery.skipSuffixes\n ),\n skipSuffixesExtra: [],\n exampleFirst:\n user.discovery?.exampleFirst ?? DEFAULT_CONFIG.discovery.exampleFirst\n },\n base: {\n name: user.base?.name ?? DEFAULT_CONFIG.base.name,\n priority: user.base?.priority ?? DEFAULT_CONFIG.base.priority\n },\n heuristics: {\n secretTokens: pickList(\n user.heuristics?.secretTokens,\n user.heuristics?.secretTokensExtra,\n DEFAULT_CONFIG.heuristics.secretTokens\n ),\n secretTokensExtra: [],\n placeholders: pickList(\n user.heuristics?.placeholders,\n user.heuristics?.placeholdersExtra,\n DEFAULT_CONFIG.heuristics.placeholders\n ),\n placeholdersExtra: [],\n grouping: user.heuristics?.grouping ?? DEFAULT_CONFIG.heuristics.grouping\n },\n diff: defu(user.diff, DEFAULT_CONFIG.diff),\n tui: {\n theme: defu(user.tui?.theme, DEFAULT_CONFIG.tui.theme),\n layout: defu(user.tui?.layout, DEFAULT_CONFIG.tui.layout),\n undoLimit: user.tui?.undoLimit ?? DEFAULT_CONFIG.tui.undoLimit,\n maskSecrets: user.tui?.maskSecrets ?? DEFAULT_CONFIG.tui.maskSecrets\n }\n };\n}\n\nexport interface ResolvedHeuristics {\n isSecretKey(key: string): boolean;\n isPlaceholderValue(value: string): boolean;\n grouping: GroupingMode;\n}\n\n/**\n * Compile the resolved heuristics config into ready-to-call matchers. Secret\n * tokens are upper-cased (the matcher upper-cases keys); placeholder atoms are\n * compiled into a single case-insensitive alternation. RGBA-free.\n */\nexport function resolveHeuristics(c: EnvprismConfig): ResolvedHeuristics {\n const tokens = c.heuristics.secretTokens.map((t) => t.toUpperCase());\n const placeholderRe = new RegExp(\n `^(${c.heuristics.placeholders.join('|')})$`,\n 'i'\n );\n return {\n isSecretKey: (key) => isSecretKey(key, tokens),\n isPlaceholderValue: (value) => isPlaceholderValue(value, placeholderRe),\n grouping: c.heuristics.grouping\n };\n}\n","import { existsSync } from 'node:fs';\nimport { loadConfig } from 'c12';\nimport { dirname, join } from 'pathe';\nimport { mergeConfig } from '@/config/resolve.ts';\nimport type { EnvprismConfig, EnvprismUserConfig } from '@/config/schema.ts';\n\nconst CONFIG_EXTS = ['ts', 'js', 'mjs', 'json'] as const;\n\n/**\n * Walk up from `start` looking for envprism.config.{ts,js,mjs,json}. c12 does\n * not climb ancestors itself, so we resolve the file path here and hand it to\n * c12 explicitly. Returns the first match or undefined at the filesystem root.\n */\nfunction findConfigUp(start: string): string | undefined {\n let dir = start;\n for (;;) {\n for (const ext of CONFIG_EXTS) {\n const candidate = join(dir, `envprism.config.${ext}`);\n if (existsSync(candidate)) return candidate;\n }\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\nexport interface LoadOptions {\n /** Directory to start the walk-up search from (default: process.cwd()). */\n cwd?: string;\n /** Explicit config file path; overrides walk-up and the env var. */\n configFile?: string;\n}\n\nexport interface LoadedConfig {\n config: EnvprismConfig;\n /** Absolute path of the config file that was loaded, if any. */\n configFile: string | undefined;\n /** Directory the search ran from. */\n cwd: string;\n}\n\n/**\n * Load and resolve the envprism config. Precedence for *where* to look:\n * explicit `configFile` > `ENVPRISM_CONFIG` env var > walk-up from cwd.\n * c12 handles the walk-up (envprism.config.{ts,js,mjs,json}); we keep\n * rc/global/dotenv off and apply our own merge (defu deep-merges defaults\n * into user lists, which we don't want — see resolve.ts).\n */\nexport async function loadEnvprismConfig(\n options: LoadOptions = {}\n): Promise<LoadedConfig> {\n const envConfig = process.env.ENVPRISM_CONFIG;\n const startCwd = options.cwd ?? process.cwd();\n // Precedence: explicit flag > env var > walk-up from cwd.\n const resolved = options.configFile ?? envConfig ?? findConfigUp(startCwd);\n const cwd = resolved ? dirname(resolved) : startCwd;\n\n if (!resolved) {\n return { config: mergeConfig({}), configFile: undefined, cwd };\n }\n\n const { config } = await loadConfig<EnvprismUserConfig>({\n name: 'envprism',\n cwd,\n configFile: resolved,\n rcFile: false,\n globalRc: false,\n dotenv: false\n });\n\n return {\n config: mergeConfig(config ?? {}),\n configFile: resolved,\n cwd\n };\n}\n","import { basename } from 'pathe';\nimport type { EnvFile } from '@/core/types.ts';\n\nexport interface BaseOptions {\n /** Filename treated as the default base when present (default `.env.example`). */\n name?: string;\n /** Ordered basenames tried before falling back to the first file. */\n priority?: readonly string[];\n}\n\n/**\n * Resolve which {@link EnvFile} acts as the base (reference) for diff. Order:\n *\n * 1. `override` path argument, matched by full path or basename.\n * 2. `options.name` (default `.env.example`) if present.\n * 3. The first file matching `options.priority`, in priority order.\n * 4. The first file (already sorted by {@link discoverEnvFiles}).\n *\n * Returns `null` if `files` is empty.\n */\nexport function resolveBase(\n files: EnvFile[],\n override?: string,\n options: BaseOptions = {}\n): EnvFile | null {\n if (files.length === 0) return null;\n\n if (override) {\n const match = files.find(\n (f) => f.path === override || basename(f.path) === override\n );\n if (!match) {\n throw new Error(\n `--base ${override} did not match any discovered env file`\n );\n }\n return match;\n }\n\n const name = options.name ?? '.env.example';\n const example = files.find((f) => basename(f.path) === name);\n if (example) return example;\n\n for (const candidate of options.priority ?? []) {\n const match = files.find((f) => basename(f.path) === candidate);\n if (match) return match;\n }\n\n return files[0] ?? null;\n}\n","import { basename } from 'pathe';\nimport type { Matrix, CellState } from '@/core/matrix.ts';\nimport type { EnvFile } from '@/core/types.ts';\n\nexport interface DiffReport {\n base: string;\n /** Compared files, excluding the base. */\n files: DiffFileReport[];\n /** True when no non-base cell is `differs`, `missing`, or `extra`. */\n inSync: boolean;\n}\n\nexport interface DiffFileReport {\n path: string;\n keys: Record<string, CellState>;\n /** Number of keys with state !== 'same'. */\n drift: number;\n}\n\nconst DRIFT_STATES: ReadonlySet<CellState> = new Set([\n 'differs',\n 'missing',\n 'extra'\n]);\n\nexport function computeDiff(matrix: Matrix): DiffReport {\n const others = matrix.files.filter((f) => f !== matrix.base);\n const files = others.map((f) => buildFileReport(matrix, f));\n return {\n base: matrix.base.path,\n files,\n inSync: files.every((f) => f.drift === 0)\n };\n}\n\nfunction buildFileReport(matrix: Matrix, file: EnvFile): DiffFileReport {\n const keys: Record<string, CellState> = {};\n let drift = 0;\n for (const key of matrix.keys) {\n const { state } = matrix.cell(key, file);\n keys[key] = state;\n if (DRIFT_STATES.has(state)) drift++;\n }\n return { path: file.path, keys, drift };\n}\n\nexport function formatDiffText(report: DiffReport): string {\n const baseName = basename(report.base);\n const otherNames = report.files.map((f) => basename(f.path));\n const lines: string[] = [];\n lines.push(`Base: ${baseName} (vs. ${otherNames.join(', ')})`);\n lines.push('');\n\n if (report.files.length === 0) {\n lines.push('No other env files to compare.');\n return lines.join('\\n') + '\\n';\n }\n\n const driftKeys = new Set<string>();\n for (const f of report.files) {\n for (const [k, s] of Object.entries(f.keys)) {\n if (DRIFT_STATES.has(s)) driftKeys.add(k);\n }\n }\n\n if (driftKeys.size === 0) {\n lines.push('All env files are in sync with the base.');\n return lines.join('\\n') + '\\n';\n }\n\n const keyWidth = Math.max(3, ...[...driftKeys].map((k) => k.length));\n const colWidth = Math.max(12, ...otherNames.map((n) => n.length));\n\n lines.push(\n formatRow('KEY', otherNames, keyWidth, colWidth, (n) => n.padEnd(colWidth))\n );\n for (const key of [...driftKeys].sort()) {\n const cells = report.files.map((f) => stateLabel(f.keys[key] ?? 'missing'));\n lines.push(\n formatRow(key, cells, keyWidth, colWidth, (n) => n.padEnd(colWidth))\n );\n }\n\n lines.push('');\n const totalDrift = report.files.reduce((sum, f) => sum + f.drift, 0);\n lines.push(\n `${driftKeys.size} key(s) differ across ${report.files.length} file(s) (${totalDrift} cell drift).`\n );\n\n return lines.join('\\n') + '\\n';\n}\n\nfunction formatRow(\n key: string,\n cells: string[],\n keyWidth: number,\n colWidth: number,\n pad: (s: string) => string\n): string {\n return [key.padEnd(keyWidth), ...cells.map(pad)].join(' ');\n}\n\nfunction stateLabel(state: CellState): string {\n switch (state) {\n case 'same':\n return '— same';\n case 'differs':\n return '≠ differs';\n case 'missing':\n return '✗ missing';\n case 'extra':\n return '★ extra';\n case 'base':\n return '· base';\n }\n}\n","import type { EnvEntry, EnvFile, KvEntry, Quoting } from '@/core/types.ts';\n\nconst KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Parse a `.env` file into a structured, round-trippable representation.\n * Each entry stores its original raw line so the serializer can emit it\n * byte-for-byte when nothing changes.\n */\nexport function parseEnv(source: string, path = ''): EnvFile {\n const trailingNewline = source.endsWith('\\n');\n const body = trailingNewline ? source.slice(0, -1) : source;\n const lines = body.length === 0 ? [] : body.split('\\n');\n\n const entries: EnvEntry[] = [];\n for (const raw of lines) {\n entries.push(parseLine(raw));\n }\n\n return { path, entries, trailingNewline };\n}\n\nfunction parseLine(raw: string): EnvEntry {\n if (raw.trim().length === 0) {\n return { kind: 'blank', raw };\n }\n\n // Comment line: leading whitespace then `#`.\n const trimmedStart = raw.replace(/^\\s+/, '');\n if (trimmedStart.startsWith('#')) {\n return { kind: 'comment', raw };\n }\n\n const kv = tryParseKv(raw);\n if (kv) return kv;\n\n // Unrecognised line — treat as a comment-like passthrough so round-trip\n // still works. Diff/matrix code ignores non-kv entries.\n return { kind: 'comment', raw };\n}\n\nfunction tryParseKv(raw: string): KvEntry | null {\n let rest = raw;\n // Optional leading whitespace is preserved in `raw`; the structured key\n // identifier itself is captured separately so we can rebuild the line.\n const leadingWs = rest.match(/^[ \\t]*/)?.[0] ?? '';\n rest = rest.slice(leadingWs.length);\n\n let exportPrefix = false;\n if (rest.startsWith('export ')) {\n exportPrefix = true;\n rest = rest.slice('export '.length).replace(/^[ \\t]*/, '');\n }\n\n const eqIdx = rest.indexOf('=');\n if (eqIdx < 0) return null;\n\n const key = rest.slice(0, eqIdx).trimEnd();\n if (!KEY_RE.test(key)) return null;\n\n const after = rest.slice(eqIdx + 1);\n const valueStart = after.replace(/^[ \\t]*/, '');\n const valueLeadingWs = after.slice(0, after.length - valueStart.length);\n\n const parsed = parseValue(valueStart);\n if (!parsed) return null;\n\n // Preserve trailing whitespace before any inline comment so round-trip\n // matches the source exactly (we don't need to model it separately — `raw`\n // is what gets emitted).\n void leadingWs;\n void valueLeadingWs;\n\n return {\n kind: 'kv',\n key,\n rawValue: parsed.rawValue,\n value: parsed.value,\n quoting: parsed.quoting,\n exportPrefix,\n inlineComment: parsed.inlineComment,\n raw\n };\n}\n\ninterface ParsedValue {\n rawValue: string;\n value: string;\n quoting: Quoting;\n inlineComment: string;\n}\n\nfunction parseValue(input: string): ParsedValue | null {\n if (input.length === 0) {\n return { rawValue: '', value: '', quoting: 'none', inlineComment: '' };\n }\n\n const first = input[0];\n if (first === '\"' || first === \"'\") {\n const close = findClosingQuote(input, first);\n if (close < 0) return null;\n const rawValue = input.slice(1, close);\n const value = first === '\"' ? decodeDoubleQuoted(rawValue) : rawValue;\n const tail = input.slice(close + 1);\n const inlineComment = extractInlineComment(tail);\n return {\n rawValue,\n value,\n quoting: first === '\"' ? 'double' : 'single',\n inlineComment\n };\n }\n\n // Unquoted: value runs until ` #` or end-of-line, trailing whitespace\n // belongs to the inline-comment slot (so round-trip is preserved via `raw`).\n const hashIdx = findUnquotedCommentStart(input);\n const valuePart = (hashIdx < 0 ? input : input.slice(0, hashIdx)).trimEnd();\n const inlineComment = hashIdx < 0 ? '' : input.slice(valuePart.length);\n return {\n rawValue: valuePart,\n value: valuePart,\n quoting: 'none',\n inlineComment\n };\n}\n\nfunction findClosingQuote(input: string, quote: '\"' | \"'\"): number {\n for (let i = 1; i < input.length; i++) {\n const ch = input[i];\n if (quote === '\"' && ch === '\\\\') {\n i++;\n continue;\n }\n if (ch === quote) return i;\n }\n return -1;\n}\n\nfunction findUnquotedCommentStart(input: string): number {\n for (let i = 0; i < input.length; i++) {\n if (input[i] !== '#') continue;\n if (i === 0) return i;\n const prev = input[i - 1];\n if (prev === ' ' || prev === '\\t') return i;\n }\n return -1;\n}\n\nfunction extractInlineComment(tail: string): string {\n // Tail is everything after the closing quote. It is either empty, pure\n // whitespace, or whitespace + `#...`. Preserve it verbatim.\n return tail;\n}\n\nfunction decodeDoubleQuoted(raw: string): string {\n return raw.replace(/\\\\(.)/g, (_, ch: string) => {\n switch (ch) {\n case 'n':\n return '\\n';\n case 'r':\n return '\\r';\n case 't':\n return '\\t';\n case '\\\\':\n return '\\\\';\n case '\"':\n return '\"';\n default:\n return `\\\\${ch}`;\n }\n });\n}\n","import { readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, join } from 'pathe';\nimport { parseEnv } from '@/core/parse.ts';\nimport type { EnvFile } from '@/core/types.ts';\n\nconst SKIP_SUFFIXES = ['.swp', '~', '.bak'];\n\nexport interface DiscoverOptions {\n /** Filename suffixes to skip (default: editor/backup suffixes). */\n skipSuffixes?: readonly string[];\n /** Sort `.env.example` first in the result (default: true). */\n exampleFirst?: boolean;\n}\n\n/**\n * Discover `.env*` files in the given path(s). Each path may be a directory\n * (glob-like discovery happens at its top level) or an explicit file. Editor\n * and backup files are skipped.\n *\n * Files are returned sorted: `.env.example` first if present (unless\n * `exampleFirst` is false), then the rest alphabetically. Base-resolution\n * lives in `base.ts` and depends on this order.\n */\nexport async function discoverEnvFiles(\n paths: string[],\n options: DiscoverOptions = {}\n): Promise<EnvFile[]> {\n const skipSuffixes = options.skipSuffixes ?? SKIP_SUFFIXES;\n const exampleFirst = options.exampleFirst ?? true;\n const filePaths = new Set<string>();\n\n for (const p of paths) {\n const info = await stat(p);\n if (info.isDirectory()) {\n const entries = await readdir(p);\n for (const name of entries) {\n if (!looksLikeEnvFile(name, skipSuffixes)) continue;\n filePaths.add(join(p, name));\n }\n } else {\n filePaths.add(p);\n }\n }\n\n const sorted = [...filePaths].sort((a, b) =>\n envPathOrder(a, b, exampleFirst)\n );\n const files: EnvFile[] = [];\n for (const filePath of sorted) {\n const source = await readFile(filePath, 'utf8');\n files.push(parseEnv(source, filePath));\n }\n return files;\n}\n\nfunction looksLikeEnvFile(\n name: string,\n skipSuffixes: readonly string[]\n): boolean {\n if (!name.startsWith('.env')) return false;\n if (skipSuffixes.some((s) => name.endsWith(s))) return false;\n return true;\n}\n\nfunction envPathOrder(a: string, b: string, exampleFirst: boolean): number {\n const an = basename(a);\n const bn = basename(b);\n if (exampleFirst) {\n if (an === '.env.example') return -1;\n if (bn === '.env.example') return 1;\n }\n return an.localeCompare(bn);\n}\n","import type { EnvEntry, EnvFile } from '@/core/types.ts';\n\nconst SEP_CHARS = new Set(['=', '-', '#', '~', '*']);\nconst SPACE_CHARS = new Set([' ', '\\t']);\n\n/**\n * Walk the base file in source order and decide which section each kv entry\n * belongs to. Returns a `key → section` map; keys with no inferred section\n * are absent from the map.\n *\n * A \"section header\" is one of:\n * 1. An inline banner: `# === Section name ===` (or `---`, `~~~`, `***`).\n * 2. A block banner — a single comment line `# Section name` whose\n * immediately preceding or following comment line is purely decorative\n * (e.g. `# ===========================`).\n *\n * The detected name applies to every subsequent kv entry until the next\n * detected section header.\n */\nexport function computeSections(base: EnvFile): Map<string, string> {\n const out = new Map<string, string>();\n let current: string | null = null;\n for (let i = 0; i < base.entries.length; i++) {\n const name = detectSectionName(base.entries, i);\n if (name !== null) current = name;\n const e = base.entries[i]!;\n if (e.kind === 'kv' && current) out.set(e.key, current);\n }\n return out;\n}\n\nfunction detectSectionName(entries: EnvEntry[], idx: number): string | null {\n const e = entries[idx];\n if (!e || e.kind !== 'comment') return null;\n if (isDecorativeLine(e.raw)) return null;\n\n const inline = parseInlineBanner(e.raw);\n if (inline !== null) return inline;\n\n const text = stripCommentPrefix(e.raw);\n if (!text) return null;\n\n if (isDecorative(entries[idx - 1]) || isDecorative(entries[idx + 1])) {\n return text;\n }\n return null;\n}\n\nfunction isDecorative(e: EnvEntry | undefined): boolean {\n if (!e || e.kind !== 'comment') return false;\n return isDecorativeLine(e.raw);\n}\n\n/**\n * True for comment lines whose body is nothing but separator chars and\n * whitespace (e.g. \"# ======\", \"###\", \"# -=-=-=-\").\n *\n * Implemented with a manual scan rather than a regex with overlapping\n * `\\s*` / `[\\s…]+` groups, which CodeQL flags as ReDoS-prone.\n */\nfunction isDecorativeLine(raw: string): boolean {\n const start = skipSpaces(raw, 0);\n if (start >= raw.length || raw[start] !== '#') return false;\n let i = start + 1;\n let sawSeparator = false;\n while (i < raw.length) {\n const ch = raw[i]!;\n if (SEP_CHARS.has(ch)) sawSeparator = true;\n else if (!SPACE_CHARS.has(ch)) return false;\n i++;\n }\n return sawSeparator;\n}\n\n/**\n * Parse a single-line inline banner like `# === Name ===` (also `---`,\n * `~~~`, `***`) and return the inner name, or null if the line isn't an\n * inline banner.\n *\n * Manual scan, again to avoid the overlapping `\\s*` regex backtracking\n * CodeQL warned about.\n */\nfunction parseInlineBanner(raw: string): string | null {\n const start = skipSpaces(raw, 0);\n if (start >= raw.length || raw[start] !== '#') return null;\n\n // After '#', skip spaces, then read 2+ separator chars (single char repeated).\n let i = skipSpaces(raw, start + 1);\n if (i >= raw.length || !SEP_CHARS.has(raw[i]!)) return null;\n let leadCount = 0;\n while (i < raw.length && SEP_CHARS.has(raw[i]!)) {\n leadCount++;\n i++;\n }\n if (leadCount < 2) return null;\n\n // Walk from the end inward to find the trailing separator run.\n let j = raw.length;\n while (j > i && SPACE_CHARS.has(raw[j - 1]!)) j--;\n if (j <= i || !SEP_CHARS.has(raw[j - 1]!)) return null;\n let trailCount = 0;\n while (j > i && SEP_CHARS.has(raw[j - 1]!)) {\n trailCount++;\n j--;\n }\n if (trailCount < 2) return null;\n\n // Between the two separator runs lives the name (after stripping spaces).\n const inner = raw.slice(i, j).trim();\n return inner.length > 0 ? inner : null;\n}\n\nfunction stripCommentPrefix(raw: string): string {\n // Leading whitespace, then one or more '#', then whitespace, then content.\n let i = skipSpaces(raw, 0);\n while (i < raw.length && raw[i] === '#') i++;\n i = skipSpaces(raw, i);\n // Trim trailing whitespace and decorative chars.\n let j = raw.length;\n while (\n j > i &&\n (SPACE_CHARS.has(raw[j - 1]!) || SEP_CHARS.has(raw[j - 1]!))\n ) {\n j--;\n }\n return raw.slice(i, j);\n}\n\nfunction skipSpaces(raw: string, from: number): number {\n let i = from;\n while (i < raw.length && SPACE_CHARS.has(raw[i]!)) i++;\n return i;\n}\n","import { computeSections } from '@/core/sections.ts';\nimport type { EnvFile, KvEntry } from '@/core/types.ts';\n\nexport type CellState = 'same' | 'differs' | 'missing' | 'extra' | 'base';\n\nexport interface Cell {\n state: CellState;\n /** Decoded value if present in this file, else `undefined`. */\n value: string | undefined;\n}\n\nexport interface Matrix {\n /** Union of all keys across all files, base keys first (in source order). */\n keys: string[];\n /** Files in display order; base is included. */\n files: EnvFile[];\n base: EnvFile;\n cell(key: string, file: EnvFile): Cell;\n /**\n * Section name inferred from the base file's comment banners, or\n * `undefined` for keys outside any section (and for keys only present in\n * non-base files).\n */\n sectionOf(key: string): string | undefined;\n}\n\nexport function buildMatrix(files: EnvFile[], base: EnvFile): Matrix {\n const keys = collectKeys(files, base);\n const lookups = new Map<EnvFile, Map<string, KvEntry>>();\n for (const file of files) {\n lookups.set(file, indexKv(file));\n }\n const baseIndex = lookups.get(base);\n if (!baseIndex) {\n throw new Error('base file is not in the files list');\n }\n const sections = computeSections(base);\n\n return {\n keys,\n files,\n base,\n sectionOf(key) {\n return sections.get(key);\n },\n cell(key, file) {\n const ownIndex = lookups.get(file);\n if (!ownIndex) {\n throw new Error(`file not in matrix: ${file.path}`);\n }\n const own = ownIndex.get(key);\n const baseEntry = baseIndex.get(key);\n\n if (file === base) {\n return {\n state: own ? 'base' : 'missing',\n value: own?.value\n };\n }\n\n if (!own && !baseEntry) {\n return { state: 'missing', value: undefined };\n }\n if (!own) {\n return { state: 'missing', value: undefined };\n }\n if (!baseEntry) {\n return { state: 'extra', value: own.value };\n }\n return {\n state: own.value === baseEntry.value ? 'same' : 'differs',\n value: own.value\n };\n }\n };\n}\n\nfunction collectKeys(files: EnvFile[], base: EnvFile): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n\n // Base file keys first in their authored order so the matrix view tracks\n // the .env.example layout most users curate.\n for (const e of base.entries) {\n if (e.kind === 'kv' && !seen.has(e.key)) {\n seen.add(e.key);\n out.push(e.key);\n }\n }\n // Then any extras only present in non-base files, in alpha order so the\n // result is deterministic.\n const extras = new Set<string>();\n for (const file of files) {\n if (file === base) continue;\n for (const e of file.entries) {\n if (e.kind === 'kv' && !seen.has(e.key)) {\n extras.add(e.key);\n }\n }\n }\n for (const k of [...extras].sort()) out.push(k);\n return out;\n}\n\nfunction indexKv(file: EnvFile): Map<string, KvEntry> {\n const m = new Map<string, KvEntry>();\n for (const e of file.entries) {\n if (e.kind === 'kv') m.set(e.key, e);\n }\n return m;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//#region src/core/serialize.ts
|
|
2
|
+
/**
|
|
3
|
+
* Serialize an {@link EnvFile} back to a string. Unchanged entries are emitted
|
|
4
|
+
* from their captured `raw` line so round-trip is byte-exact; modified entries
|
|
5
|
+
* must have their `raw` field re-built via {@link rebuildKvLine} first.
|
|
6
|
+
*/
|
|
7
|
+
function serializeEnv(file) {
|
|
8
|
+
const body = file.entries.map((e) => e.raw).join("\n");
|
|
9
|
+
return file.trailingNewline ? `${body}\n` : body;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Rebuild the `raw` line of a kv entry from its structured fields. Call this
|
|
13
|
+
* after mutating `key`, `value`, `quoting`, or `exportPrefix` so the next
|
|
14
|
+
* serialize emits the new line.
|
|
15
|
+
*/
|
|
16
|
+
function rebuildKvLine(entry) {
|
|
17
|
+
const prefix = entry.exportPrefix ? "export " : "";
|
|
18
|
+
const encoded = encodeValue(entry.value, entry.quoting);
|
|
19
|
+
entry.rawValue = encoded.inner;
|
|
20
|
+
entry.raw = `${prefix}${entry.key}=${encoded.full}${entry.inlineComment}`;
|
|
21
|
+
}
|
|
22
|
+
function encodeValue(value, quoting) {
|
|
23
|
+
switch (needsQuoting(value, quoting) ? "double" : quoting) {
|
|
24
|
+
case "none": return {
|
|
25
|
+
inner: value,
|
|
26
|
+
full: value
|
|
27
|
+
};
|
|
28
|
+
case "single":
|
|
29
|
+
if (value.includes("'")) {
|
|
30
|
+
const escaped = escapeDoubleQuoted(value);
|
|
31
|
+
return {
|
|
32
|
+
inner: escaped,
|
|
33
|
+
full: `"${escaped}"`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
inner: value,
|
|
38
|
+
full: `'${value}'`
|
|
39
|
+
};
|
|
40
|
+
case "double": {
|
|
41
|
+
const escaped = escapeDoubleQuoted(value);
|
|
42
|
+
return {
|
|
43
|
+
inner: escaped,
|
|
44
|
+
full: `"${escaped}"`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function needsQuoting(value, current) {
|
|
50
|
+
if (current !== "none") return false;
|
|
51
|
+
return /[\s#"'\\]/.test(value);
|
|
52
|
+
}
|
|
53
|
+
function escapeDoubleQuoted(value) {
|
|
54
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { serializeEnv as n, rebuildKvLine as t };
|
|
58
|
+
|
|
59
|
+
//# sourceMappingURL=serialize-DWF9VxrS.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize-DWF9VxrS.mjs","names":[],"sources":["../../src/core/serialize.ts"],"sourcesContent":["import type { EnvFile, KvEntry, Quoting } from '@/core/types.ts';\n\n/**\n * Serialize an {@link EnvFile} back to a string. Unchanged entries are emitted\n * from their captured `raw` line so round-trip is byte-exact; modified entries\n * must have their `raw` field re-built via {@link rebuildKvLine} first.\n */\nexport function serializeEnv(file: EnvFile): string {\n const body = file.entries.map((e) => e.raw).join('\\n');\n return file.trailingNewline ? `${body}\\n` : body;\n}\n\n/**\n * Rebuild the `raw` line of a kv entry from its structured fields. Call this\n * after mutating `key`, `value`, `quoting`, or `exportPrefix` so the next\n * serialize emits the new line.\n */\nexport function rebuildKvLine(entry: KvEntry): void {\n const prefix = entry.exportPrefix ? 'export ' : '';\n const encoded = encodeValue(entry.value, entry.quoting);\n // Update rawValue to match the (possibly new) encoded inner form.\n entry.rawValue = encoded.inner;\n entry.raw = `${prefix}${entry.key}=${encoded.full}${entry.inlineComment}`;\n}\n\ninterface EncodedValue {\n /** Inner string without surrounding quotes (for `rawValue`). */\n inner: string;\n /** Fully formed value including surrounding quotes if any. */\n full: string;\n}\n\nfunction encodeValue(value: string, quoting: Quoting): EncodedValue {\n // Auto-promote to double quotes when an unquoted value would be ambiguous\n // (contains whitespace, `#`, or characters that require escaping). Single\n // quotes are preserved as authored but can't represent embedded `'`.\n const effective: Quoting = needsQuoting(value, quoting) ? 'double' : quoting;\n\n switch (effective) {\n case 'none':\n return { inner: value, full: value };\n case 'single': {\n if (value.includes(\"'\")) {\n // Fall back to double-quoting if a single quote sneaks in.\n const escaped = escapeDoubleQuoted(value);\n return { inner: escaped, full: `\"${escaped}\"` };\n }\n return { inner: value, full: `'${value}'` };\n }\n case 'double': {\n const escaped = escapeDoubleQuoted(value);\n return { inner: escaped, full: `\"${escaped}\"` };\n }\n }\n}\n\nfunction needsQuoting(value: string, current: Quoting): boolean {\n if (current !== 'none') return false;\n return /[\\s#\"'\\\\]/.test(value);\n}\n\nfunction escapeDoubleQuoted(value: string): string {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .replace(/\\t/g, '\\\\t');\n}\n"],"mappings":";;;;;;AAOA,SAAgB,aAAa,MAAuB;CAClD,MAAM,OAAO,KAAK,QAAQ,KAAK,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI;CACrD,OAAO,KAAK,kBAAkB,GAAG,KAAK,MAAM;AAC9C;;;;;;AAOA,SAAgB,cAAc,OAAsB;CAClD,MAAM,SAAS,MAAM,eAAe,YAAY;CAChD,MAAM,UAAU,YAAY,MAAM,OAAO,MAAM,OAAO;CAEtD,MAAM,WAAW,QAAQ;CACzB,MAAM,MAAM,GAAG,SAAS,MAAM,IAAI,GAAG,QAAQ,OAAO,MAAM;AAC5D;AASA,SAAS,YAAY,OAAe,SAAgC;CAMlE,QAF2B,aAAa,OAAO,OAAO,IAAI,WAAW,SAErE;EACE,KAAK,QACH,OAAO;GAAE,OAAO;GAAO,MAAM;EAAM;EACrC,KAAK;GACH,IAAI,MAAM,SAAS,GAAG,GAAG;IAEvB,MAAM,UAAU,mBAAmB,KAAK;IACxC,OAAO;KAAE,OAAO;KAAS,MAAM,IAAI,QAAQ;IAAG;GAChD;GACA,OAAO;IAAE,OAAO;IAAO,MAAM,IAAI,MAAM;GAAG;EAE5C,KAAK,UAAU;GACb,MAAM,UAAU,mBAAmB,KAAK;GACxC,OAAO;IAAE,OAAO;IAAS,MAAM,IAAI,QAAQ;GAAG;EAChD;CACF;AACF;AAEA,SAAS,aAAa,OAAe,SAA2B;CAC9D,IAAI,YAAY,QAAQ,OAAO;CAC/B,OAAO,YAAY,KAAK,KAAK;AAC/B;AAEA,SAAS,mBAAmB,OAAuB;CACjD,OAAO,MACJ,QAAQ,OAAO,MAAM,CAAC,CACtB,QAAQ,MAAM,MAAK,CAAC,CACpB,QAAQ,OAAO,KAAK,CAAC,CACrB,QAAQ,OAAO,KAAK,CAAC,CACrB,QAAQ,OAAO,KAAK;AACzB"}
|
package/dist/config/define.mjs
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
//#region src/config/define.ts
|
|
2
|
+
/**
|
|
3
|
+
* Identity helper for type-safe config authoring:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { defineEnvprismConfig } from 'envprism/config';
|
|
7
|
+
* export default defineEnvprismConfig({ heuristics: { grouping: 'prefix' } });
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
1
10
|
function defineEnvprismConfig(config) {
|
|
2
|
-
|
|
11
|
+
return config;
|
|
3
12
|
}
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//# sourceMappingURL=define.mjs.map
|
|
13
|
+
//#endregion
|
|
14
|
+
export { defineEnvprismConfig };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=define.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define.mjs","sources":["../../src/config/define.ts"],"sourcesContent":["// Public, dependency-light entry for `envprism/config`. Types + identity\n// helper only — must never import c12, defu, or @opentui/core so config\n// authoring stays cheap to load.\n\nimport type { EnvprismUserConfig } from '@/config/schema.ts';\n\nexport type {\n BaseConfig,\n DiffConfig,\n DiscoveryConfig,\n EnvprismConfig,\n EnvprismUserConfig,\n GroupingMode,\n HeuristicsConfig,\n LayoutConfig,\n ThemeConfig,\n TuiConfig\n} from '@/config/schema.ts';\n\n/**\n * Identity helper for type-safe config authoring:\n *\n * ```ts\n * import { defineEnvprismConfig } from 'envprism/config';\n * export default defineEnvprismConfig({ heuristics: { grouping: 'prefix' } });\n * ```\n */\nexport function defineEnvprismConfig(\n config: EnvprismUserConfig\n): EnvprismUserConfig {\n return config;\n}\n"],"
|
|
1
|
+
{"version":3,"file":"define.mjs","names":[],"sources":["../../src/config/define.ts"],"sourcesContent":["// Public, dependency-light entry for `envprism/config`. Types + identity\n// helper only — must never import c12, defu, or @opentui/core so config\n// authoring stays cheap to load.\n\nimport type { EnvprismUserConfig } from '@/config/schema.ts';\n\nexport type {\n BaseConfig,\n DiffConfig,\n DiscoveryConfig,\n EnvprismConfig,\n EnvprismUserConfig,\n GroupingMode,\n HeuristicsConfig,\n LayoutConfig,\n ThemeConfig,\n TuiConfig\n} from '@/config/schema.ts';\n\n/**\n * Identity helper for type-safe config authoring:\n *\n * ```ts\n * import { defineEnvprismConfig } from 'envprism/config';\n * export default defineEnvprismConfig({ heuristics: { grouping: 'prefix' } });\n * ```\n */\nexport function defineEnvprismConfig(\n config: EnvprismUserConfig\n): EnvprismUserConfig {\n return config;\n}\n"],"mappings":";;;;;;;;;AA2BA,SAAgB,qBACd,QACoB;CACpB,OAAO;AACT"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { _ as isSecretKey, a as computeDiff, c as loadEnvprismConfig, i as parseEnv, n as computeSections, o as formatDiffText, r as discoverEnvFiles, s as resolveBase, t as buildMatrix, v as maskValue } from "./chunks/matrix-CG1Msljr.mjs";
|
|
2
|
+
import { n as serializeEnv, t as rebuildKvLine } from "./chunks/serialize-DWF9VxrS.mjs";
|
|
3
3
|
import { defineEnvprismConfig } from "./config/define.mjs";
|
|
4
|
-
export {
|
|
5
|
-
b as buildMatrix,
|
|
6
|
-
c as computeDiff,
|
|
7
|
-
d as computeSections,
|
|
8
|
-
defineEnvprismConfig,
|
|
9
|
-
e as discoverEnvFiles,
|
|
10
|
-
g as formatDiffText,
|
|
11
|
-
i as isSecretKey,
|
|
12
|
-
l as loadEnvprismConfig,
|
|
13
|
-
m as maskValue,
|
|
14
|
-
p as parseEnv,
|
|
15
|
-
r2 as rebuildKvLine,
|
|
16
|
-
r as resolveBase,
|
|
17
|
-
s as serializeEnv
|
|
18
|
-
};
|
|
19
|
-
//# sourceMappingURL=index.mjs.map
|
|
4
|
+
export { buildMatrix, computeDiff, computeSections, defineEnvprismConfig, discoverEnvFiles, formatDiffText, isSecretKey, loadEnvprismConfig, maskValue, parseEnv, rebuildKvLine, resolveBase, serializeEnv };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "envprism",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"description": "TUI-based env file manager — refract one set of variables into many environment views.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"bun",
|
|
@@ -52,26 +52,26 @@
|
|
|
52
52
|
"provenance": true
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@opentui/core": "^0.
|
|
56
|
-
"c12": "^3.
|
|
57
|
-
"citty": "^0.
|
|
58
|
-
"consola": "^3.4.
|
|
59
|
-
"defu": "^6.1.
|
|
60
|
-
"pathe": "^2.0.
|
|
55
|
+
"@opentui/core": "^0.4.5",
|
|
56
|
+
"c12": "^3.3.4",
|
|
57
|
+
"citty": "^0.2.2",
|
|
58
|
+
"consola": "^3.4.2",
|
|
59
|
+
"defu": "^6.1.7",
|
|
60
|
+
"pathe": "^2.0.3"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@commitlint/cli": "^21.
|
|
64
|
-
"@commitlint/config-conventional": "^21.0
|
|
65
|
-
"@types/node": "^
|
|
66
|
-
"@vitest/coverage-v8": "^
|
|
63
|
+
"@commitlint/cli": "^21.2.1",
|
|
64
|
+
"@commitlint/config-conventional": "^21.2.0",
|
|
65
|
+
"@types/node": "^26.1.1",
|
|
66
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
67
67
|
"husky": "^9.1.7",
|
|
68
|
-
"lint-staged": "^17.0.
|
|
68
|
+
"lint-staged": "^17.0.8",
|
|
69
69
|
"oxfmt": "0.51.0",
|
|
70
70
|
"oxlint": "1.66.0",
|
|
71
|
-
"taze": "^19.
|
|
72
|
-
"typescript": "^
|
|
73
|
-
"vite": "^
|
|
74
|
-
"vitest": "^
|
|
71
|
+
"taze": "^19.14.1",
|
|
72
|
+
"typescript": "^7.0.2",
|
|
73
|
+
"vite": "^8.1.5",
|
|
74
|
+
"vitest": "^4.1.10"
|
|
75
75
|
},
|
|
76
76
|
"engines": {
|
|
77
77
|
"bun": ">=1.3"
|