@kaynooo/env-tools 0.0.0-stage → 0.4.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/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@kaynooo/env-tools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "type": "module",
4
+ "version": "0.4.1",
5
+ "description": "ev lists and edits .env files with secrets masked, redact masks secrets in any piped output.",
6
+ "author": "Kaynooo <kaynooo@kevyn.fr>",
7
+ "license": "MIT",
8
+ "homepage": "https://git.kevyn.fr/kaynooo/agent-tools/src/branch/main/packages/env#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://git.kevyn.fr/kaynooo/agent-tools.git",
12
+ "directory": "packages/env"
13
+ },
14
+ "bugs": "https://git.kevyn.fr/kaynooo/agent-tools/issues",
15
+ "keywords": ["env", "dotenv", "secrets", "redact", "mask"],
16
+ "exports": {
17
+ "./dotenv": "./src/dotenv.ts",
18
+ "./guard": "./src/guard.ts"
19
+ },
20
+ "bin": {
21
+ "ev": "./dist/ev.js",
22
+ "redact": "./dist/redact.js"
23
+ },
24
+ "files": ["dist", "src/dotenv.ts"],
25
+ "engines": {
26
+ "bun": ">=1.4.3"
27
+ },
28
+ "publishConfig": {
29
+ "registry": "https://git.kevyn.fr/api/packages/kaynooo/npm/"
30
+ },
31
+ "scripts": {
32
+ "build": "$npm_execpath build src/ev.ts src/redact.ts --target bun --outdir dist",
33
+ "ev": "$npm_execpath src/ev.ts",
34
+ "redact": "$npm_execpath src/redact.ts"
35
+ },
36
+ "devDependencies": {
37
+ "@kaynooo/cli-kit": "workspace:*"
38
+ }
39
+ }
package/src/dotenv.ts ADDED
@@ -0,0 +1,152 @@
1
+ // One logical line of a .env file: a comment, a blank or an unparsed line (no key), or an assignment whose raw text may span several physical lines.
2
+ export interface DotenvLine {
3
+ raw: string
4
+ key?: string
5
+ value?: string
6
+ }
7
+
8
+ export interface Dotenv {
9
+ lines: DotenvLine[]
10
+ }
11
+
12
+ const ASSIGNMENT = /^\s*(?:export\s+)?([A-Z_][\w.-]*)\s*=(.*)$/i
13
+ const EXPORTED = /^\s*export\s/
14
+ const INLINE_COMMENT = /\s+#.*$/
15
+ const CARRIAGE_RETURN = /\r$/
16
+ const ESCAPES: Record<string, string> = { 'n': '\n', 'r': '\r', 't': '\t', '"': '"', '\\': '\\' }
17
+ const NEEDS_QUOTES = /[\s#"'\\$`]/
18
+
19
+ // Finds the closing quote, skipping backslash escapes inside double quotes. Returns -1 when the value continues on the next line.
20
+ function closingQuote(text: string, quote: string): number {
21
+ for (let index = 0; index < text.length; index++) {
22
+ if (quote === '"' && text[index] === '\\')
23
+ index++
24
+ else if (text[index] === quote)
25
+ return index
26
+ }
27
+
28
+ return -1
29
+ }
30
+
31
+ function unescapeDouble(text: string): string {
32
+ let out = ''
33
+
34
+ for (let index = 0; index < text.length; index++) {
35
+ const next = text[index + 1]
36
+
37
+ if (text[index] === '\\' && next !== undefined && next in ESCAPES) {
38
+ out += ESCAPES[next]
39
+ index++
40
+ }
41
+ else {
42
+ out += text[index]
43
+ }
44
+ }
45
+
46
+ return out
47
+ }
48
+
49
+ export function parseDotenv(text: string): Dotenv {
50
+ const physical = text.split('\n')
51
+ const lines: DotenvLine[] = []
52
+
53
+ for (let index = 0; index < physical.length; index++) {
54
+ const line = physical[index] ?? ''
55
+ const match = ASSIGNMENT.exec(line.replace(CARRIAGE_RETURN, ''))
56
+
57
+ if (match === null) {
58
+ lines.push({ raw: line })
59
+
60
+ continue
61
+ }
62
+
63
+ const [, key = '', rest = ''] = match
64
+ const start = rest.trimStart()
65
+ const quote = start[0]
66
+
67
+ if (quote !== '"' && quote !== '\'' && quote !== '`') {
68
+ lines.push({ raw: line, key, value: start.replace(INLINE_COMMENT, '').trim() })
69
+
70
+ continue
71
+ }
72
+
73
+ let body = start.slice(1)
74
+ let raw = line
75
+ let end = closingQuote(body, quote)
76
+
77
+ while (end === -1 && index + 1 < physical.length) {
78
+ index++
79
+ body += `\n${(physical[index] ?? '').replace(CARRIAGE_RETURN, '')}`
80
+ raw += `\n${physical[index] ?? ''}`
81
+ end = closingQuote(body, quote)
82
+ }
83
+
84
+ const inner = end === -1 ? body : body.slice(0, end)
85
+
86
+ lines.push({ raw, key, value: quote === '"' ? unescapeDouble(inner) : inner })
87
+ }
88
+
89
+ return { lines }
90
+ }
91
+
92
+ export function serializeDotenv(file: Dotenv): string {
93
+ return file.lines.map(line => line.raw).join('\n')
94
+ }
95
+
96
+ // The effective entries: a repeated key keeps its last value, as dotenv loaders do.
97
+ export function entries(file: Dotenv): Map<string, string> {
98
+ const map = new Map<string, string>()
99
+
100
+ for (const line of file.lines) {
101
+ if (line.key !== undefined)
102
+ map.set(line.key, line.value ?? '')
103
+ }
104
+
105
+ return map
106
+ }
107
+
108
+ export function formatAssignment(key: string, value: string): string {
109
+ if (value === '' || !NEEDS_QUOTES.test(value))
110
+ return `${key}=${value}`
111
+
112
+ const escaped = value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\r', '\\r').replaceAll('\n', '\\n')
113
+
114
+ return `${key}="${escaped}"`
115
+ }
116
+
117
+ // Replaces the last assignment of a key keeping its `export`, or appends before the trailing newline. `raw` is the whole line to store when given.
118
+ export function setLine(file: Dotenv, key: string, value: string, raw?: string): void {
119
+ const index = file.lines.findLastIndex(line => line.key === key)
120
+ const existing = file.lines[index]
121
+
122
+ if (existing !== undefined) {
123
+ const exported = EXPORTED.test(existing.raw) ? 'export ' : ''
124
+
125
+ file.lines[index] = { raw: raw ?? `${exported}${formatAssignment(key, value)}`, key, value }
126
+
127
+ return
128
+ }
129
+
130
+ const line = { raw: raw ?? formatAssignment(key, value), key, value }
131
+ const last = file.lines.at(-1)
132
+
133
+ if (last === undefined)
134
+ file.lines.push(line, { raw: '' })
135
+ else if (last.raw === '')
136
+ file.lines.splice(-1, 0, line)
137
+ else
138
+ file.lines.push(line, { raw: '' })
139
+ }
140
+
141
+ // Drops every assignment of the key and returns how many went.
142
+ export function removeKey(file: Dotenv, key: string): number {
143
+ const before = file.lines.length
144
+
145
+ file.lines = file.lines.filter(line => line.key !== key)
146
+
147
+ return before - file.lines.length
148
+ }
149
+
150
+ export function lastLine(file: Dotenv, key: string): DotenvLine | undefined {
151
+ return file.lines.findLast(line => line.key === key)
152
+ }