@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/LICENSE +21 -0
- package/README.md +45 -2
- package/dist/ev.js +1405 -0
- package/dist/redact.js +936 -0
- package/package.json +37 -4
- package/src/dotenv.ts +152 -0
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaynooo/env-tools",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
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
|
+
}
|