@townco/env 0.1.3 → 0.1.4
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/dist/env-file.d.ts +105 -0
- package/dist/env-file.js +205 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +19 -0
- package/dist/update-schema.d.ts +5 -0
- package/dist/update-schema.js +32 -0
- package/package.json +6 -3
- package/src/env-file.ts +0 -248
- package/src/index.ts +0 -22
- package/src/update-schema.ts +0 -47
- package/tsconfig.json +0 -6
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a line in an .env file
|
|
3
|
+
*/
|
|
4
|
+
type EnvLine = {
|
|
5
|
+
type: "comment";
|
|
6
|
+
content: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: "blank";
|
|
9
|
+
} | {
|
|
10
|
+
type: "entry";
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
raw: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A data structure that represents a parsed .env file while preserving
|
|
17
|
+
* all original content including comments, blank lines, and order.
|
|
18
|
+
*/
|
|
19
|
+
export declare class EnvFile {
|
|
20
|
+
private lines;
|
|
21
|
+
constructor(lines?: EnvLine[]);
|
|
22
|
+
/**
|
|
23
|
+
* Parse a .env file string into an EnvFile structure
|
|
24
|
+
*/
|
|
25
|
+
static parse(content: string): EnvFile;
|
|
26
|
+
/**
|
|
27
|
+
* Serialize the EnvFile back to a string
|
|
28
|
+
*/
|
|
29
|
+
toString(): string;
|
|
30
|
+
/**
|
|
31
|
+
* Get all entries as a key-value record
|
|
32
|
+
*/
|
|
33
|
+
toRecord(): Record<string, string>;
|
|
34
|
+
/**
|
|
35
|
+
* Find an entry line by key
|
|
36
|
+
*/
|
|
37
|
+
private findEntry;
|
|
38
|
+
/**
|
|
39
|
+
* Find the index of an entry by key
|
|
40
|
+
*/
|
|
41
|
+
private findEntryIndex;
|
|
42
|
+
/**
|
|
43
|
+
* Create an entry line from key and value
|
|
44
|
+
*/
|
|
45
|
+
private createEntry;
|
|
46
|
+
/**
|
|
47
|
+
* Normalize a value by ensuring it's properly quoted
|
|
48
|
+
*/
|
|
49
|
+
private normalizeValue;
|
|
50
|
+
/**
|
|
51
|
+
* Find the insertion index for a new entry (before the final blank line if present)
|
|
52
|
+
*/
|
|
53
|
+
private findInsertionIndex;
|
|
54
|
+
/**
|
|
55
|
+
* Get the value for a specific key
|
|
56
|
+
*/
|
|
57
|
+
get(key: string): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Set a value for a key. If the key exists, updates it in place.
|
|
60
|
+
* If it doesn't exist, appends it above the final newline (if present).
|
|
61
|
+
* Values are always double quoted.
|
|
62
|
+
*/
|
|
63
|
+
set(key: string, value: string): this;
|
|
64
|
+
/**
|
|
65
|
+
* Delete a key-value entry
|
|
66
|
+
*/
|
|
67
|
+
delete(key: string): this;
|
|
68
|
+
/**
|
|
69
|
+
* Check if a key exists
|
|
70
|
+
*/
|
|
71
|
+
has(key: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Get all keys
|
|
74
|
+
*/
|
|
75
|
+
keys(): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Iterate over all entries
|
|
78
|
+
*/
|
|
79
|
+
entries(): IterableIterator<[string, string]>;
|
|
80
|
+
/**
|
|
81
|
+
* Apply a function to all entries and return a new EnvFile
|
|
82
|
+
*/
|
|
83
|
+
map(fn: (key: string, value: string) => [string, string]): EnvFile;
|
|
84
|
+
/**
|
|
85
|
+
* Filter entries based on a predicate
|
|
86
|
+
*/
|
|
87
|
+
filter(fn: (key: string, value: string) => boolean): EnvFile;
|
|
88
|
+
/**
|
|
89
|
+
* Add a comment line
|
|
90
|
+
*/
|
|
91
|
+
addComment(content: string): this;
|
|
92
|
+
/**
|
|
93
|
+
* Add a blank line
|
|
94
|
+
*/
|
|
95
|
+
addBlank(): this;
|
|
96
|
+
/**
|
|
97
|
+
* Get the raw lines array for custom operations
|
|
98
|
+
*/
|
|
99
|
+
getLines(): readonly EnvLine[];
|
|
100
|
+
/**
|
|
101
|
+
* Create a clone of this EnvFile
|
|
102
|
+
*/
|
|
103
|
+
clone(): EnvFile;
|
|
104
|
+
}
|
|
105
|
+
export {};
|
package/dist/env-file.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A data structure that represents a parsed .env file while preserving
|
|
3
|
+
* all original content including comments, blank lines, and order.
|
|
4
|
+
*/
|
|
5
|
+
export class EnvFile {
|
|
6
|
+
lines;
|
|
7
|
+
constructor(lines = []) {
|
|
8
|
+
this.lines = lines;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse a .env file string into an EnvFile structure
|
|
12
|
+
*/
|
|
13
|
+
static parse(content) {
|
|
14
|
+
const lines = content.split("\n").map((line) => {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
if (trimmed === "") {
|
|
17
|
+
return { type: "blank" };
|
|
18
|
+
}
|
|
19
|
+
if (trimmed.startsWith("#")) {
|
|
20
|
+
return { type: "comment", content: line };
|
|
21
|
+
}
|
|
22
|
+
const equalsIndex = line.indexOf("=");
|
|
23
|
+
if (equalsIndex === -1) {
|
|
24
|
+
// Malformed line, treat as comment
|
|
25
|
+
return { type: "comment", content: line };
|
|
26
|
+
}
|
|
27
|
+
const key = line.substring(0, equalsIndex).trim();
|
|
28
|
+
const value = line.substring(equalsIndex + 1);
|
|
29
|
+
return { type: "entry", key, value, raw: line };
|
|
30
|
+
});
|
|
31
|
+
return new EnvFile(lines);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Serialize the EnvFile back to a string
|
|
35
|
+
*/
|
|
36
|
+
toString() {
|
|
37
|
+
return this.lines
|
|
38
|
+
.map((line) => {
|
|
39
|
+
switch (line.type) {
|
|
40
|
+
case "blank":
|
|
41
|
+
return "";
|
|
42
|
+
case "comment":
|
|
43
|
+
return line.content;
|
|
44
|
+
case "entry":
|
|
45
|
+
return line.raw;
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`Unknown line type`);
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.join("\n");
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get all entries as a key-value record
|
|
54
|
+
*/
|
|
55
|
+
toRecord() {
|
|
56
|
+
return Object.fromEntries(this.entries());
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Find an entry line by key
|
|
60
|
+
*/
|
|
61
|
+
findEntry(key) {
|
|
62
|
+
const line = this.lines.find((l) => l.type === "entry" && l.key === key);
|
|
63
|
+
return line;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Find the index of an entry by key
|
|
67
|
+
*/
|
|
68
|
+
findEntryIndex(key) {
|
|
69
|
+
return this.lines.findIndex((l) => l.type === "entry" && l.key === key);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create an entry line from key and value
|
|
73
|
+
*/
|
|
74
|
+
createEntry(key, value) {
|
|
75
|
+
const quotedValue = this.normalizeValue(value);
|
|
76
|
+
return {
|
|
77
|
+
type: "entry",
|
|
78
|
+
key,
|
|
79
|
+
value: quotedValue,
|
|
80
|
+
raw: `${key}=${quotedValue}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Normalize a value by ensuring it's properly quoted
|
|
85
|
+
*/
|
|
86
|
+
normalizeValue(value) {
|
|
87
|
+
// Strip existing quotes if present, then add quotes
|
|
88
|
+
const unquoted = value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
|
|
89
|
+
return `"${unquoted}"`;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Find the insertion index for a new entry (before the final blank line if present)
|
|
93
|
+
*/
|
|
94
|
+
findInsertionIndex() {
|
|
95
|
+
const lastLine = this.lines[this.lines.length - 1];
|
|
96
|
+
return lastLine?.type === "blank"
|
|
97
|
+
? this.lines.length - 1
|
|
98
|
+
: this.lines.length;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get the value for a specific key
|
|
102
|
+
*/
|
|
103
|
+
get(key) {
|
|
104
|
+
return this.findEntry(key)?.value;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Set a value for a key. If the key exists, updates it in place.
|
|
108
|
+
* If it doesn't exist, appends it above the final newline (if present).
|
|
109
|
+
* Values are always double quoted.
|
|
110
|
+
*/
|
|
111
|
+
set(key, value) {
|
|
112
|
+
const index = this.findEntryIndex(key);
|
|
113
|
+
const entry = this.createEntry(key, value);
|
|
114
|
+
if (index !== -1) {
|
|
115
|
+
this.lines[index] = entry;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
this.lines.splice(this.findInsertionIndex(), 0, entry);
|
|
119
|
+
}
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Delete a key-value entry
|
|
124
|
+
*/
|
|
125
|
+
delete(key) {
|
|
126
|
+
this.lines = this.lines.filter((l) => !(l.type === "entry" && l.key === key));
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Check if a key exists
|
|
131
|
+
*/
|
|
132
|
+
has(key) {
|
|
133
|
+
return this.findEntry(key) !== undefined;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get all keys
|
|
137
|
+
*/
|
|
138
|
+
keys() {
|
|
139
|
+
return this.lines
|
|
140
|
+
.filter((l) => l.type === "entry")
|
|
141
|
+
.map((l) => l.key);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Iterate over all entries
|
|
145
|
+
*/
|
|
146
|
+
*entries() {
|
|
147
|
+
for (const line of this.lines) {
|
|
148
|
+
if (line.type === "entry") {
|
|
149
|
+
yield [line.key, line.value];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Apply a function to all entries and return a new EnvFile
|
|
155
|
+
*/
|
|
156
|
+
map(fn) {
|
|
157
|
+
const newLines = this.lines.map((line) => {
|
|
158
|
+
if (line.type === "entry") {
|
|
159
|
+
const [newKey, newValue] = fn(line.key, line.value);
|
|
160
|
+
return this.createEntry(newKey, newValue);
|
|
161
|
+
}
|
|
162
|
+
return line;
|
|
163
|
+
});
|
|
164
|
+
return new EnvFile(newLines);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Filter entries based on a predicate
|
|
168
|
+
*/
|
|
169
|
+
filter(fn) {
|
|
170
|
+
const newLines = this.lines.filter((line) => {
|
|
171
|
+
if (line.type === "entry") {
|
|
172
|
+
return fn(line.key, line.value);
|
|
173
|
+
}
|
|
174
|
+
return true; // Keep comments and blank lines
|
|
175
|
+
});
|
|
176
|
+
return new EnvFile(newLines);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Add a comment line
|
|
180
|
+
*/
|
|
181
|
+
addComment(content) {
|
|
182
|
+
const comment = content.startsWith("#") ? content : `# ${content}`;
|
|
183
|
+
this.lines.push({ type: "comment", content: comment });
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Add a blank line
|
|
188
|
+
*/
|
|
189
|
+
addBlank() {
|
|
190
|
+
this.lines.push({ type: "blank" });
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get the raw lines array for custom operations
|
|
195
|
+
*/
|
|
196
|
+
getLines() {
|
|
197
|
+
return this.lines;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Create a clone of this EnvFile
|
|
201
|
+
*/
|
|
202
|
+
clone() {
|
|
203
|
+
return new EnvFile([...this.lines]);
|
|
204
|
+
}
|
|
205
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const envSchema: z.ZodObject<{
|
|
3
|
+
ANTHROPIC_API_KEY: z.ZodString;
|
|
4
|
+
AWS_ACCESS_KEY_ID: z.ZodString;
|
|
5
|
+
AWS_SECRET_ACCESS_KEY: z.ZodString;
|
|
6
|
+
EXA_API_KEY: z.ZodString;
|
|
7
|
+
FLY_TOKEN: z.ZodString;
|
|
8
|
+
NEXT_PUBLIC_SITE_URL: z.ZodString;
|
|
9
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.ZodString;
|
|
10
|
+
NEXT_PUBLIC_SUPABASE_URL: z.ZodString;
|
|
11
|
+
SUPABASE_ACCESS_TOKEN: z.ZodString;
|
|
12
|
+
SUPABASE_ANON_KEY: z.ZodString;
|
|
13
|
+
SUPABASE_DB_PASSWORD: z.ZodString;
|
|
14
|
+
SUPABASE_PROJECT_ID: z.ZodString;
|
|
15
|
+
SUPABASE_SRC_ARC_BUCKET: z.ZodString;
|
|
16
|
+
SUPABASE_URL: z.ZodString;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export type Env = z.infer<typeof envSchema>;
|
|
19
|
+
export declare const parseEnv: (env?: NodeJS.ProcessEnv) => {
|
|
20
|
+
ANTHROPIC_API_KEY: string;
|
|
21
|
+
AWS_ACCESS_KEY_ID: string;
|
|
22
|
+
AWS_SECRET_ACCESS_KEY: string;
|
|
23
|
+
EXA_API_KEY: string;
|
|
24
|
+
FLY_TOKEN: string;
|
|
25
|
+
NEXT_PUBLIC_SITE_URL: string;
|
|
26
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: string;
|
|
27
|
+
NEXT_PUBLIC_SUPABASE_URL: string;
|
|
28
|
+
SUPABASE_ACCESS_TOKEN: string;
|
|
29
|
+
SUPABASE_ANON_KEY: string;
|
|
30
|
+
SUPABASE_DB_PASSWORD: string;
|
|
31
|
+
SUPABASE_PROJECT_ID: string;
|
|
32
|
+
SUPABASE_SRC_ARC_BUCKET: string;
|
|
33
|
+
SUPABASE_URL: string;
|
|
34
|
+
};
|
|
35
|
+
export { EnvFile } from "./env-file";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const envSchema = z.object({
|
|
3
|
+
ANTHROPIC_API_KEY: z.string().min(1),
|
|
4
|
+
AWS_ACCESS_KEY_ID: z.string().min(1),
|
|
5
|
+
AWS_SECRET_ACCESS_KEY: z.string().min(1),
|
|
6
|
+
EXA_API_KEY: z.string().min(1),
|
|
7
|
+
FLY_TOKEN: z.string().min(1),
|
|
8
|
+
NEXT_PUBLIC_SITE_URL: z.string().min(1),
|
|
9
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
|
|
10
|
+
NEXT_PUBLIC_SUPABASE_URL: z.string().min(1),
|
|
11
|
+
SUPABASE_ACCESS_TOKEN: z.string().min(1),
|
|
12
|
+
SUPABASE_ANON_KEY: z.string().min(1),
|
|
13
|
+
SUPABASE_DB_PASSWORD: z.string().min(1),
|
|
14
|
+
SUPABASE_PROJECT_ID: z.string().min(1),
|
|
15
|
+
SUPABASE_SRC_ARC_BUCKET: z.string().min(1),
|
|
16
|
+
SUPABASE_URL: z.string().min(1),
|
|
17
|
+
});
|
|
18
|
+
export const parseEnv = (env = process.env) => envSchema.parse(env);
|
|
19
|
+
export { EnvFile } from "./env-file";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Biome } from "@biomejs/js-api/nodejs";
|
|
5
|
+
import { findRoot } from "@townco/core";
|
|
6
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
7
|
+
const envSchemaTsSrc = path.join("packages", "env", "src", "index.ts");
|
|
8
|
+
export const updateSchema = async ({ envSchemaSrc = envSchemaTsSrc, ...opts } = {}) => {
|
|
9
|
+
const envFile = opts.envFile ?? path.join(await findRoot(), ".env.in");
|
|
10
|
+
const root = await findRoot();
|
|
11
|
+
const keys = (await Bun.file(envFile).text())
|
|
12
|
+
.split("\n")
|
|
13
|
+
.filter((line) => line.match(/^[A-Z_]+\=/))
|
|
14
|
+
.map((line) => line.split("=")[0])
|
|
15
|
+
.filter((k) => Boolean(k))
|
|
16
|
+
.sort();
|
|
17
|
+
const src = new Project().addSourceFileAtPath(envSchemaSrc);
|
|
18
|
+
const obj = src
|
|
19
|
+
.getVariableDeclaration("envSchema")
|
|
20
|
+
.getInitializerIfKindOrThrow(SyntaxKind.CallExpression)
|
|
21
|
+
.getArguments()[0]
|
|
22
|
+
?.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
23
|
+
obj?.getProperties().forEach((p) => p.remove());
|
|
24
|
+
obj?.addPropertyAssignments(keys.map((name) => ({ name, initializer: "z.string().min(1)" })));
|
|
25
|
+
const biome = new Biome();
|
|
26
|
+
src.replaceWithText(biome.formatContent(biome.openProject(root).projectKey, src.getFullText(), {
|
|
27
|
+
filePath: src.getFilePath(),
|
|
28
|
+
}).content);
|
|
29
|
+
await src.save();
|
|
30
|
+
};
|
|
31
|
+
if (import.meta.main)
|
|
32
|
+
await updateSchema();
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/env",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.4",
|
|
5
5
|
"description": "env",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"repository": "github:townco/town",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
10
13
|
"exports": {
|
|
11
14
|
".": {
|
|
12
15
|
"import": "./dist/index.js",
|
|
@@ -18,7 +21,7 @@
|
|
|
18
21
|
}
|
|
19
22
|
},
|
|
20
23
|
"devDependencies": {
|
|
21
|
-
"@townco/tsconfig": "0.1.
|
|
24
|
+
"@townco/tsconfig": "0.1.51",
|
|
22
25
|
"ts-morph": "^27.0.2",
|
|
23
26
|
"typescript": "^5.9.3"
|
|
24
27
|
},
|
|
@@ -28,7 +31,7 @@
|
|
|
28
31
|
"schema:update": "bun src/update-schema"
|
|
29
32
|
},
|
|
30
33
|
"dependencies": {
|
|
31
|
-
"@townco/core": "0.0.
|
|
34
|
+
"@townco/core": "0.0.32",
|
|
32
35
|
"zod": "^4.1.13"
|
|
33
36
|
}
|
|
34
37
|
}
|
package/src/env-file.ts
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Represents a line in an .env file
|
|
3
|
-
*/
|
|
4
|
-
type EnvLine =
|
|
5
|
-
| { type: "comment"; content: string }
|
|
6
|
-
| { type: "blank" }
|
|
7
|
-
| { type: "entry"; key: string; value: string; raw: string };
|
|
8
|
-
|
|
9
|
-
type EnvEntry = Extract<EnvLine, { type: "entry" }>;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* A data structure that represents a parsed .env file while preserving
|
|
13
|
-
* all original content including comments, blank lines, and order.
|
|
14
|
-
*/
|
|
15
|
-
export class EnvFile {
|
|
16
|
-
private lines: EnvLine[];
|
|
17
|
-
|
|
18
|
-
constructor(lines: EnvLine[] = []) {
|
|
19
|
-
this.lines = lines;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Parse a .env file string into an EnvFile structure
|
|
24
|
-
*/
|
|
25
|
-
static parse(content: string): EnvFile {
|
|
26
|
-
const lines: EnvLine[] = content.split("\n").map((line): EnvLine => {
|
|
27
|
-
const trimmed = line.trim();
|
|
28
|
-
|
|
29
|
-
if (trimmed === "") {
|
|
30
|
-
return { type: "blank" };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (trimmed.startsWith("#")) {
|
|
34
|
-
return { type: "comment", content: line };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const equalsIndex = line.indexOf("=");
|
|
38
|
-
if (equalsIndex === -1) {
|
|
39
|
-
// Malformed line, treat as comment
|
|
40
|
-
return { type: "comment", content: line };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const key = line.substring(0, equalsIndex).trim();
|
|
44
|
-
const value = line.substring(equalsIndex + 1);
|
|
45
|
-
|
|
46
|
-
return { type: "entry", key, value, raw: line };
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
return new EnvFile(lines);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Serialize the EnvFile back to a string
|
|
54
|
-
*/
|
|
55
|
-
toString(): string {
|
|
56
|
-
return this.lines
|
|
57
|
-
.map((line) => {
|
|
58
|
-
switch (line.type) {
|
|
59
|
-
case "blank":
|
|
60
|
-
return "";
|
|
61
|
-
case "comment":
|
|
62
|
-
return line.content;
|
|
63
|
-
case "entry":
|
|
64
|
-
return line.raw;
|
|
65
|
-
default:
|
|
66
|
-
throw new Error(`Unknown line type`);
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
.join("\n");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Get all entries as a key-value record
|
|
74
|
-
*/
|
|
75
|
-
toRecord(): Record<string, string> {
|
|
76
|
-
return Object.fromEntries(this.entries());
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Find an entry line by key
|
|
81
|
-
*/
|
|
82
|
-
private findEntry(key: string): EnvEntry | undefined {
|
|
83
|
-
const line = this.lines.find((l) => l.type === "entry" && l.key === key);
|
|
84
|
-
return line as EnvEntry | undefined;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Find the index of an entry by key
|
|
89
|
-
*/
|
|
90
|
-
private findEntryIndex(key: string): number {
|
|
91
|
-
return this.lines.findIndex((l) => l.type === "entry" && l.key === key);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Create an entry line from key and value
|
|
96
|
-
*/
|
|
97
|
-
private createEntry(key: string, value: string): EnvEntry {
|
|
98
|
-
const quotedValue = this.normalizeValue(value);
|
|
99
|
-
return {
|
|
100
|
-
type: "entry",
|
|
101
|
-
key,
|
|
102
|
-
value: quotedValue,
|
|
103
|
-
raw: `${key}=${quotedValue}`,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Normalize a value by ensuring it's properly quoted
|
|
109
|
-
*/
|
|
110
|
-
private normalizeValue(value: string): string {
|
|
111
|
-
// Strip existing quotes if present, then add quotes
|
|
112
|
-
const unquoted =
|
|
113
|
-
value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
|
|
114
|
-
return `"${unquoted}"`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Find the insertion index for a new entry (before the final blank line if present)
|
|
119
|
-
*/
|
|
120
|
-
private findInsertionIndex(): number {
|
|
121
|
-
const lastLine = this.lines[this.lines.length - 1];
|
|
122
|
-
return lastLine?.type === "blank"
|
|
123
|
-
? this.lines.length - 1
|
|
124
|
-
: this.lines.length;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Get the value for a specific key
|
|
129
|
-
*/
|
|
130
|
-
get(key: string): string | undefined {
|
|
131
|
-
return this.findEntry(key)?.value;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Set a value for a key. If the key exists, updates it in place.
|
|
136
|
-
* If it doesn't exist, appends it above the final newline (if present).
|
|
137
|
-
* Values are always double quoted.
|
|
138
|
-
*/
|
|
139
|
-
set(key: string, value: string): this {
|
|
140
|
-
const index = this.findEntryIndex(key);
|
|
141
|
-
const entry = this.createEntry(key, value);
|
|
142
|
-
|
|
143
|
-
if (index !== -1) {
|
|
144
|
-
this.lines[index] = entry;
|
|
145
|
-
} else {
|
|
146
|
-
this.lines.splice(this.findInsertionIndex(), 0, entry);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return this;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Delete a key-value entry
|
|
154
|
-
*/
|
|
155
|
-
delete(key: string): this {
|
|
156
|
-
this.lines = this.lines.filter(
|
|
157
|
-
(l) => !(l.type === "entry" && l.key === key),
|
|
158
|
-
);
|
|
159
|
-
return this;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Check if a key exists
|
|
164
|
-
*/
|
|
165
|
-
has(key: string): boolean {
|
|
166
|
-
return this.findEntry(key) !== undefined;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Get all keys
|
|
171
|
-
*/
|
|
172
|
-
keys(): string[] {
|
|
173
|
-
return this.lines
|
|
174
|
-
.filter((l): l is EnvEntry => l.type === "entry")
|
|
175
|
-
.map((l) => l.key);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Iterate over all entries
|
|
180
|
-
*/
|
|
181
|
-
*entries(): IterableIterator<[string, string]> {
|
|
182
|
-
for (const line of this.lines) {
|
|
183
|
-
if (line.type === "entry") {
|
|
184
|
-
yield [line.key, line.value];
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Apply a function to all entries and return a new EnvFile
|
|
191
|
-
*/
|
|
192
|
-
map(fn: (key: string, value: string) => [string, string]): EnvFile {
|
|
193
|
-
const newLines = this.lines.map((line) => {
|
|
194
|
-
if (line.type === "entry") {
|
|
195
|
-
const [newKey, newValue] = fn(line.key, line.value);
|
|
196
|
-
return this.createEntry(newKey, newValue);
|
|
197
|
-
}
|
|
198
|
-
return line;
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
return new EnvFile(newLines);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Filter entries based on a predicate
|
|
206
|
-
*/
|
|
207
|
-
filter(fn: (key: string, value: string) => boolean): EnvFile {
|
|
208
|
-
const newLines = this.lines.filter((line) => {
|
|
209
|
-
if (line.type === "entry") {
|
|
210
|
-
return fn(line.key, line.value);
|
|
211
|
-
}
|
|
212
|
-
return true; // Keep comments and blank lines
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
return new EnvFile(newLines);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Add a comment line
|
|
220
|
-
*/
|
|
221
|
-
addComment(content: string): this {
|
|
222
|
-
const comment = content.startsWith("#") ? content : `# ${content}`;
|
|
223
|
-
this.lines.push({ type: "comment", content: comment });
|
|
224
|
-
return this;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Add a blank line
|
|
229
|
-
*/
|
|
230
|
-
addBlank(): this {
|
|
231
|
-
this.lines.push({ type: "blank" });
|
|
232
|
-
return this;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Get the raw lines array for custom operations
|
|
237
|
-
*/
|
|
238
|
-
getLines(): readonly EnvLine[] {
|
|
239
|
-
return this.lines;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Create a clone of this EnvFile
|
|
244
|
-
*/
|
|
245
|
-
clone(): EnvFile {
|
|
246
|
-
return new EnvFile([...this.lines]);
|
|
247
|
-
}
|
|
248
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const envSchema = z.object({
|
|
4
|
-
ANTHROPIC_API_KEY: z.string().min(1),
|
|
5
|
-
AWS_ACCESS_KEY_ID: z.string().min(1),
|
|
6
|
-
AWS_SECRET_ACCESS_KEY: z.string().min(1),
|
|
7
|
-
EXA_API_KEY: z.string().min(1),
|
|
8
|
-
FLY_TOKEN: z.string().min(1),
|
|
9
|
-
NEXT_PUBLIC_SITE_URL: z.string().min(1),
|
|
10
|
-
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
|
|
11
|
-
NEXT_PUBLIC_SUPABASE_URL: z.string().min(1),
|
|
12
|
-
SUPABASE_ACCESS_TOKEN: z.string().min(1),
|
|
13
|
-
SUPABASE_ANON_KEY: z.string().min(1),
|
|
14
|
-
SUPABASE_DB_PASSWORD: z.string().min(1),
|
|
15
|
-
SUPABASE_PROJECT_ID: z.string().min(1),
|
|
16
|
-
SUPABASE_SRC_ARC_BUCKET: z.string().min(1),
|
|
17
|
-
SUPABASE_URL: z.string().min(1),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
export type Env = z.infer<typeof envSchema>;
|
|
21
|
-
export const parseEnv = (env = process.env) => envSchema.parse(env);
|
|
22
|
-
export { EnvFile } from "./env-file";
|
package/src/update-schema.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { Biome } from "@biomejs/js-api/nodejs";
|
|
5
|
-
import { findRoot } from "@townco/core";
|
|
6
|
-
import { Project, SyntaxKind } from "ts-morph";
|
|
7
|
-
|
|
8
|
-
const envSchemaTsSrc = path.join("packages", "env", "src", "index.ts");
|
|
9
|
-
|
|
10
|
-
export const updateSchema = async ({
|
|
11
|
-
envSchemaSrc = envSchemaTsSrc,
|
|
12
|
-
...opts
|
|
13
|
-
}: {
|
|
14
|
-
envFile?: string;
|
|
15
|
-
envSchemaSrc?: string;
|
|
16
|
-
} = {}) => {
|
|
17
|
-
const envFile = opts.envFile ?? path.join(await findRoot(), ".env.in");
|
|
18
|
-
const root = await findRoot();
|
|
19
|
-
const keys = (await Bun.file(envFile).text())
|
|
20
|
-
.split("\n")
|
|
21
|
-
.filter((line) => line.match(/^[A-Z_]+\=/))
|
|
22
|
-
.map((line) => line.split("=")[0])
|
|
23
|
-
.filter((k): k is string => Boolean(k))
|
|
24
|
-
.sort();
|
|
25
|
-
|
|
26
|
-
const src = new Project().addSourceFileAtPath(envSchemaSrc);
|
|
27
|
-
const obj = src
|
|
28
|
-
.getVariableDeclaration("envSchema")!
|
|
29
|
-
.getInitializerIfKindOrThrow(SyntaxKind.CallExpression)
|
|
30
|
-
.getArguments()[0]
|
|
31
|
-
?.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
32
|
-
|
|
33
|
-
obj?.getProperties().forEach((p) => p.remove());
|
|
34
|
-
obj?.addPropertyAssignments(
|
|
35
|
-
keys.map((name) => ({ name, initializer: "z.string().min(1)" })),
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const biome = new Biome();
|
|
39
|
-
src.replaceWithText(
|
|
40
|
-
biome.formatContent(biome.openProject(root).projectKey, src.getFullText(), {
|
|
41
|
-
filePath: src.getFilePath(),
|
|
42
|
-
}).content,
|
|
43
|
-
);
|
|
44
|
-
await src.save();
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
if (import.meta.main) await updateSchema();
|