@bladedev/envguard 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bladedevoff
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # envguard
2
+
3
+ Type-safe `.env` for Node.js. Define a schema, get validation + TypeScript types + CLI.
4
+
5
+ Stop discovering missing env vars at runtime.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @bladedev/envguard
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ // env.schema.ts
17
+ import { envguard, t } from '@bladedev/envguard'
18
+
19
+ export default envguard({
20
+ DATABASE_URL: t.url({ required: true }),
21
+ PORT: t.port({ default: 3000 }),
22
+ NODE_ENV: t.enum(['development', 'staging', 'production']),
23
+ DEBUG: t.boolean({ default: false }),
24
+ API_KEY: t.string({ secret: true }),
25
+ })
26
+ ```
27
+
28
+ ```ts
29
+ // app.ts
30
+ import env from './env.schema'
31
+
32
+ console.log(env.PORT) // number
33
+ console.log(env.NODE_ENV) // 'development' | 'staging' | 'production'
34
+ ```
35
+
36
+ Full autocomplete. Zero runtime surprises.
37
+
38
+ ## CLI
39
+
40
+ ```bash
41
+ # Validate .env against schema
42
+ npx envguard check
43
+
44
+ # Generate schema from existing .env
45
+ npx envguard init
46
+ ```
47
+
48
+ ```
49
+ ✗ envguard: 2 problems found
50
+
51
+ DATABASE_URL ✗ required but missing
52
+ PORT ✗ "abc" is not a valid port (expected 1-65535)
53
+
54
+ 1 variable OK, 2 failed
55
+ ```
56
+
57
+ ## Types
58
+
59
+ | Helper | Output | Validates |
60
+ |---|---|---|
61
+ | `t.string()` | `string` | Non-empty |
62
+ | `t.number()` | `number` | Numeric |
63
+ | `t.boolean()` | `boolean` | true/false, 1/0, yes/no |
64
+ | `t.port()` | `number` | 1–65535 |
65
+ | `t.url()` | `string` | Valid URL |
66
+ | `t.email()` | `string` | Valid email |
67
+ | `t.enum([...])` | Literal union | One of values |
68
+ | `t.json<T>()` | `T` | Valid JSON |
69
+
70
+ Every helper accepts `{ required, default, secret }`.
71
+
72
+ ## Options
73
+
74
+ ```ts
75
+ envguard(schema, {
76
+ path: '.env', // default: reads process.env
77
+ onError: 'throw', // 'throw' | 'exit' | 'warn' (default: 'exit')
78
+ })
79
+ ```
80
+
81
+ ## vs. alternatives
82
+
83
+ | | dotenv | envalid | t3-env | **envguard** |
84
+ |---|---|---|---|---|
85
+ | .env parsing | ✓ | ✗ | ✗ | ✓ |
86
+ | Validation | ✗ | ✓ | ✓ | ✓ |
87
+ | TS inference | ✗ | Partial | ✓ | ✓ |
88
+ | CLI tools | ✗ | ✗ | ✗ | ✓ |
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { defineCommand as defineCommand3, runMain } from "citty";
5
+
6
+ // src/cli/commands/check.ts
7
+ import { defineCommand } from "citty";
8
+ import { readFileSync } from "fs";
9
+ import { resolve as resolve2 } from "path";
10
+ import { createJiti } from "jiti";
11
+
12
+ // src/core/parser.ts
13
+ function parse(input) {
14
+ const result = {};
15
+ const lines = input.split(/\r?\n/);
16
+ for (const rawLine of lines) {
17
+ const line = rawLine.trim();
18
+ if (!line || line.startsWith("#")) continue;
19
+ const stripped = line.startsWith("export ") ? line.slice(7) : line;
20
+ const eqIndex = stripped.indexOf("=");
21
+ if (eqIndex === -1) continue;
22
+ const key = stripped.slice(0, eqIndex).trim();
23
+ result[key] = parseValue(stripped.slice(eqIndex + 1));
24
+ }
25
+ return result;
26
+ }
27
+ function parseValue(raw) {
28
+ const trimmed = raw.trim();
29
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
30
+ return trimmed.slice(1, -1);
31
+ }
32
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
33
+ return trimmed.slice(1, -1).replace(/\\([nrt"\\])/g, (_, ch) => {
34
+ const map = { n: "\n", r: "\r", t: " ", '"': '"', "\\": "\\" };
35
+ return map[ch] ?? ch;
36
+ });
37
+ }
38
+ const commentIndex = trimmed.indexOf(" #");
39
+ if (commentIndex !== -1) {
40
+ return trimmed.slice(0, commentIndex).trim();
41
+ }
42
+ return trimmed;
43
+ }
44
+
45
+ // src/core/validator.ts
46
+ function validate(schema, parsed) {
47
+ const errors = [];
48
+ const env = {};
49
+ for (const [key, field] of Object.entries(schema)) {
50
+ const raw = parsed[key];
51
+ if (raw === void 0) {
52
+ if (field._default !== void 0) {
53
+ env[key] = field._default;
54
+ continue;
55
+ }
56
+ if (field._required) {
57
+ errors.push({ key, message: "required but missing" });
58
+ continue;
59
+ }
60
+ env[key] = void 0;
61
+ continue;
62
+ }
63
+ const result = field._validate(raw);
64
+ if (result.success) {
65
+ env[key] = result.value;
66
+ } else {
67
+ errors.push({ key, message: result.message, received: raw });
68
+ }
69
+ }
70
+ if (errors.length > 0) return { ok: false, errors };
71
+ return { ok: true, env };
72
+ }
73
+
74
+ // src/cli/resolve-config.ts
75
+ import { existsSync } from "fs";
76
+ import { resolve } from "path";
77
+ var CONFIG_NAMES = ["envguard.config.ts", "env.schema.ts"];
78
+ function resolveConfigPath(explicit) {
79
+ if (explicit) {
80
+ const p = resolve(explicit);
81
+ if (!existsSync(p)) throw new Error(`Config file not found: ${p}`);
82
+ return p;
83
+ }
84
+ for (const name of CONFIG_NAMES) {
85
+ const p = resolve(process.cwd(), name);
86
+ if (existsSync(p)) return p;
87
+ }
88
+ throw new Error(
89
+ `No envguard config found. Expected one of:
90
+ ${CONFIG_NAMES.map((n) => ` - ${n}`).join("\n")}`
91
+ );
92
+ }
93
+
94
+ // src/cli/reporter.ts
95
+ function formatCheckResult(errors, total) {
96
+ if (errors.length === 0) return `
97
+ \u2713 envguard: all ${total} variables OK
98
+ `;
99
+ const pad = Math.max(...errors.map((e) => e.key.length));
100
+ const lines = errors.map((e) => ` ${e.key.padEnd(pad)} \u2717 ${e.message}`);
101
+ const passed = total - errors.length;
102
+ return [
103
+ "",
104
+ `\u2717 envguard: ${errors.length} problem${errors.length > 1 ? "s" : ""} found`,
105
+ "",
106
+ ...lines,
107
+ "",
108
+ ` ${passed} variable${passed !== 1 ? "s" : ""} OK, ${errors.length} failed`,
109
+ ""
110
+ ].join("\n");
111
+ }
112
+
113
+ // src/cli/commands/check.ts
114
+ var jiti = createJiti(import.meta.url);
115
+ var checkCommand = defineCommand({
116
+ meta: { name: "check", description: "Validate .env against schema" },
117
+ args: {
118
+ config: { type: "string", description: "Path to config file" },
119
+ env: { type: "string", description: "Path to .env file", default: ".env" }
120
+ },
121
+ async run({ args }) {
122
+ const configPath = resolveConfigPath(args.config);
123
+ const mod = await jiti.import(configPath);
124
+ const schema = mod.default || mod;
125
+ const envPath = resolve2(args.env);
126
+ const content = readFileSync(envPath, "utf-8");
127
+ const parsed = parse(content);
128
+ const result = validate(schema, parsed);
129
+ const total = Object.keys(schema).length;
130
+ if (result.ok) {
131
+ process.stdout.write(formatCheckResult([], total));
132
+ process.exit(0);
133
+ } else {
134
+ process.stdout.write(formatCheckResult(result.errors, total));
135
+ process.exit(1);
136
+ }
137
+ }
138
+ });
139
+
140
+ // src/cli/commands/init.ts
141
+ import { defineCommand as defineCommand2 } from "citty";
142
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2 } from "fs";
143
+ import { resolve as resolve3 } from "path";
144
+ var SECRET_PATTERNS = ["key", "secret", "token", "password", "pass", "credential", "auth"];
145
+ function inferType(key, value) {
146
+ const lower = key.toLowerCase();
147
+ const isSecret = SECRET_PATTERNS.some((p) => lower.includes(p));
148
+ if (lower.includes("port")) return "t.port()";
149
+ try {
150
+ new URL(value);
151
+ return "t.url()";
152
+ } catch {
153
+ }
154
+ if (["true", "false", "1", "0", "yes", "no"].includes(value.toLowerCase())) return "t.boolean()";
155
+ if (value !== "" && !isNaN(Number(value))) return "t.number()";
156
+ return isSecret ? "t.string({ secret: true })" : "t.string()";
157
+ }
158
+ function generateSchema(vars) {
159
+ const fields = Object.entries(vars).map(([k, v]) => ` ${k}: ${inferType(k, v)},`).join("\n");
160
+ return `import { envguard, t } from 'envguard'
161
+
162
+ export default envguard({
163
+ ${fields}
164
+ })
165
+ `;
166
+ }
167
+ var initCommand = defineCommand2({
168
+ meta: { name: "init", description: "Generate env.schema.ts from existing .env" },
169
+ args: {
170
+ env: { type: "string", description: "Path to .env file", default: ".env" },
171
+ output: { type: "string", description: "Output file", default: "env.schema.ts" }
172
+ },
173
+ async run({ args }) {
174
+ const envPath = resolve3(args.env);
175
+ if (!existsSync2(envPath)) {
176
+ console.error(`File not found: ${envPath}`);
177
+ process.exit(1);
178
+ }
179
+ const outputPath = resolve3(args.output);
180
+ if (existsSync2(outputPath)) {
181
+ console.error(`File already exists: ${outputPath}`);
182
+ process.exit(1);
183
+ }
184
+ const parsed = parse(readFileSync2(envPath, "utf-8"));
185
+ writeFileSync(outputPath, generateSchema(parsed));
186
+ console.log(`\u2713 Scanned ${args.env} (${Object.keys(parsed).length} variables)`);
187
+ console.log(`\u2713 Generated ${args.output}`);
188
+ }
189
+ });
190
+
191
+ // src/cli/index.ts
192
+ var main = defineCommand3({
193
+ meta: { name: "envguard", version: "0.1.0", description: "Type-safe .env manager" },
194
+ subCommands: {
195
+ check: checkCommand,
196
+ init: initCommand
197
+ }
198
+ });
199
+ runMain(main);
package/dist/index.cjs ADDED
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ EnvValidationError: () => EnvValidationError,
24
+ envguard: () => envguard,
25
+ t: () => t
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+ var import_node_fs = require("fs");
29
+
30
+ // src/core/parser.ts
31
+ function parse(input) {
32
+ const result = {};
33
+ const lines = input.split(/\r?\n/);
34
+ for (const rawLine of lines) {
35
+ const line = rawLine.trim();
36
+ if (!line || line.startsWith("#")) continue;
37
+ const stripped = line.startsWith("export ") ? line.slice(7) : line;
38
+ const eqIndex = stripped.indexOf("=");
39
+ if (eqIndex === -1) continue;
40
+ const key = stripped.slice(0, eqIndex).trim();
41
+ result[key] = parseValue(stripped.slice(eqIndex + 1));
42
+ }
43
+ return result;
44
+ }
45
+ function parseValue(raw) {
46
+ const trimmed = raw.trim();
47
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
48
+ return trimmed.slice(1, -1);
49
+ }
50
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
51
+ return trimmed.slice(1, -1).replace(/\\([nrt"\\])/g, (_, ch) => {
52
+ const map = { n: "\n", r: "\r", t: " ", '"': '"', "\\": "\\" };
53
+ return map[ch] ?? ch;
54
+ });
55
+ }
56
+ const commentIndex = trimmed.indexOf(" #");
57
+ if (commentIndex !== -1) {
58
+ return trimmed.slice(0, commentIndex).trim();
59
+ }
60
+ return trimmed;
61
+ }
62
+
63
+ // src/core/validator.ts
64
+ function validate(schema, parsed) {
65
+ const errors = [];
66
+ const env = {};
67
+ for (const [key, field] of Object.entries(schema)) {
68
+ const raw = parsed[key];
69
+ if (raw === void 0) {
70
+ if (field._default !== void 0) {
71
+ env[key] = field._default;
72
+ continue;
73
+ }
74
+ if (field._required) {
75
+ errors.push({ key, message: "required but missing" });
76
+ continue;
77
+ }
78
+ env[key] = void 0;
79
+ continue;
80
+ }
81
+ const result = field._validate(raw);
82
+ if (result.success) {
83
+ env[key] = result.value;
84
+ } else {
85
+ errors.push({ key, message: result.message, received: raw });
86
+ }
87
+ }
88
+ if (errors.length > 0) return { ok: false, errors };
89
+ return { ok: true, env };
90
+ }
91
+
92
+ // src/core/errors.ts
93
+ var EnvValidationError = class extends Error {
94
+ errors;
95
+ constructor(errors) {
96
+ super(`envguard: ${errors.length} problem${errors.length > 1 ? "s" : ""} found`);
97
+ this.name = "EnvValidationError";
98
+ this.errors = errors;
99
+ }
100
+ };
101
+
102
+ // src/core/schema.ts
103
+ var import_v4 = require("zod/v4");
104
+ function createField(opts, validate2) {
105
+ return {
106
+ _type: void 0,
107
+ _required: opts.default !== void 0 ? false : opts.required ?? true,
108
+ _default: opts.default,
109
+ _secret: opts.secret ?? false,
110
+ _parse(raw) {
111
+ const r = validate2(raw);
112
+ if (!r.success) throw new Error(r.message);
113
+ return r.value;
114
+ },
115
+ _validate: validate2
116
+ };
117
+ }
118
+ var _emailSchema = null;
119
+ var t = {
120
+ string(opts = {}) {
121
+ return createField(
122
+ opts,
123
+ (raw) => raw.length > 0 ? { success: true, value: raw } : { success: false, message: "expected non-empty string" }
124
+ );
125
+ },
126
+ number(opts = {}) {
127
+ return createField(opts, (raw) => {
128
+ const n = Number(raw);
129
+ return !isNaN(n) && raw.trim() !== "" ? { success: true, value: n } : { success: false, message: `"${raw}" is not a valid number` };
130
+ });
131
+ },
132
+ boolean(opts = {}) {
133
+ const truthy = /* @__PURE__ */ new Set(["true", "1", "yes"]);
134
+ const falsy = /* @__PURE__ */ new Set(["false", "0", "no"]);
135
+ return createField(opts, (raw) => {
136
+ const v = raw.toLowerCase();
137
+ if (truthy.has(v)) return { success: true, value: true };
138
+ if (falsy.has(v)) return { success: true, value: false };
139
+ return { success: false, message: `expected true/false, 1/0, or yes/no \u2014 got "${raw}"` };
140
+ });
141
+ },
142
+ port(opts = {}) {
143
+ return createField(opts, (raw) => {
144
+ const n = Number(raw);
145
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
146
+ return { success: false, message: `"${raw}" is not a valid port (expected 1-65535)` };
147
+ }
148
+ return { success: true, value: n };
149
+ });
150
+ },
151
+ url(opts = {}) {
152
+ return createField(opts, (raw) => {
153
+ try {
154
+ new URL(raw);
155
+ return { success: true, value: raw };
156
+ } catch {
157
+ return { success: false, message: `invalid URL: "${raw}"` };
158
+ }
159
+ });
160
+ },
161
+ email(opts = {}) {
162
+ return createField(opts, (raw) => {
163
+ _emailSchema ??= import_v4.z.string().email();
164
+ return _emailSchema.safeParse(raw).success ? { success: true, value: raw } : { success: false, message: `invalid email: "${raw}"` };
165
+ });
166
+ },
167
+ enum(values, opts = {}) {
168
+ return createField(
169
+ opts,
170
+ (raw) => values.includes(raw) ? { success: true, value: raw } : { success: false, message: `"${raw}" is not one of: ${values.join(", ")}` }
171
+ );
172
+ },
173
+ json(opts = {}) {
174
+ return createField(opts, (raw) => {
175
+ try {
176
+ return { success: true, value: JSON.parse(raw) };
177
+ } catch {
178
+ return { success: false, message: `invalid JSON` };
179
+ }
180
+ });
181
+ }
182
+ };
183
+
184
+ // src/index.ts
185
+ function formatErrors(errors) {
186
+ const lines = errors.map((e) => ` ${e.key}: ${e.message}${e.received !== void 0 ? ` (got: ${e.received})` : ""}`);
187
+ return `envguard validation failed:
188
+ ${lines.join("\n")}`;
189
+ }
190
+ function envguard(schema, opts = {}) {
191
+ const { path, onError = "exit" } = opts;
192
+ const raw = path ? parse((0, import_node_fs.readFileSync)(path, "utf8")) : process.env;
193
+ const result = validate(schema, raw);
194
+ if (!result.ok) {
195
+ const msg = formatErrors(result.errors);
196
+ if (onError === "throw") throw new EnvValidationError(result.errors);
197
+ if (onError === "warn") {
198
+ console.warn(msg);
199
+ return {};
200
+ }
201
+ console.error(msg);
202
+ process.exit(1);
203
+ }
204
+ return result.env;
205
+ }
206
+ // Annotate the CommonJS export names for ESM import in node:
207
+ 0 && (module.exports = {
208
+ EnvValidationError,
209
+ envguard,
210
+ t
211
+ });
@@ -0,0 +1,51 @@
1
+ interface FieldOptions<T = unknown> {
2
+ required?: boolean;
3
+ default?: T;
4
+ secret?: boolean;
5
+ }
6
+ interface SchemaField<T = unknown> {
7
+ _type: T;
8
+ _required: boolean;
9
+ _default: T | undefined;
10
+ _secret: boolean;
11
+ _parse: (raw: string) => T;
12
+ _validate: (raw: string) => {
13
+ success: true;
14
+ value: T;
15
+ } | {
16
+ success: false;
17
+ message: string;
18
+ };
19
+ }
20
+ type Schema = Record<string, SchemaField>;
21
+ interface ValidationError {
22
+ key: string;
23
+ message: string;
24
+ received?: string;
25
+ }
26
+
27
+ declare class EnvValidationError extends Error {
28
+ errors: ValidationError[];
29
+ constructor(errors: ValidationError[]);
30
+ }
31
+
32
+ declare const t: {
33
+ string(opts?: FieldOptions<string>): SchemaField<string>;
34
+ number(opts?: FieldOptions<number>): SchemaField<number>;
35
+ boolean(opts?: FieldOptions<boolean>): SchemaField<boolean>;
36
+ port(opts?: FieldOptions<number>): SchemaField<number>;
37
+ url(opts?: FieldOptions<string>): SchemaField<string>;
38
+ email(opts?: FieldOptions<string>): SchemaField<string>;
39
+ enum<const T extends string>(values: T[], opts?: FieldOptions<T>): SchemaField<T>;
40
+ json<T = unknown>(opts?: FieldOptions<T>): SchemaField<T>;
41
+ };
42
+
43
+ interface EnvguardOptions {
44
+ path?: string;
45
+ onError?: 'throw' | 'warn' | 'exit';
46
+ }
47
+ declare function envguard<S extends Schema>(schema: S, opts?: EnvguardOptions): {
48
+ [K in keyof S]: S[K]['_type'];
49
+ };
50
+
51
+ export { EnvValidationError, type ValidationError, envguard, t };
@@ -0,0 +1,51 @@
1
+ interface FieldOptions<T = unknown> {
2
+ required?: boolean;
3
+ default?: T;
4
+ secret?: boolean;
5
+ }
6
+ interface SchemaField<T = unknown> {
7
+ _type: T;
8
+ _required: boolean;
9
+ _default: T | undefined;
10
+ _secret: boolean;
11
+ _parse: (raw: string) => T;
12
+ _validate: (raw: string) => {
13
+ success: true;
14
+ value: T;
15
+ } | {
16
+ success: false;
17
+ message: string;
18
+ };
19
+ }
20
+ type Schema = Record<string, SchemaField>;
21
+ interface ValidationError {
22
+ key: string;
23
+ message: string;
24
+ received?: string;
25
+ }
26
+
27
+ declare class EnvValidationError extends Error {
28
+ errors: ValidationError[];
29
+ constructor(errors: ValidationError[]);
30
+ }
31
+
32
+ declare const t: {
33
+ string(opts?: FieldOptions<string>): SchemaField<string>;
34
+ number(opts?: FieldOptions<number>): SchemaField<number>;
35
+ boolean(opts?: FieldOptions<boolean>): SchemaField<boolean>;
36
+ port(opts?: FieldOptions<number>): SchemaField<number>;
37
+ url(opts?: FieldOptions<string>): SchemaField<string>;
38
+ email(opts?: FieldOptions<string>): SchemaField<string>;
39
+ enum<const T extends string>(values: T[], opts?: FieldOptions<T>): SchemaField<T>;
40
+ json<T = unknown>(opts?: FieldOptions<T>): SchemaField<T>;
41
+ };
42
+
43
+ interface EnvguardOptions {
44
+ path?: string;
45
+ onError?: 'throw' | 'warn' | 'exit';
46
+ }
47
+ declare function envguard<S extends Schema>(schema: S, opts?: EnvguardOptions): {
48
+ [K in keyof S]: S[K]['_type'];
49
+ };
50
+
51
+ export { EnvValidationError, type ValidationError, envguard, t };
package/dist/index.js ADDED
@@ -0,0 +1,184 @@
1
+ // src/index.ts
2
+ import { readFileSync } from "fs";
3
+
4
+ // src/core/parser.ts
5
+ function parse(input) {
6
+ const result = {};
7
+ const lines = input.split(/\r?\n/);
8
+ for (const rawLine of lines) {
9
+ const line = rawLine.trim();
10
+ if (!line || line.startsWith("#")) continue;
11
+ const stripped = line.startsWith("export ") ? line.slice(7) : line;
12
+ const eqIndex = stripped.indexOf("=");
13
+ if (eqIndex === -1) continue;
14
+ const key = stripped.slice(0, eqIndex).trim();
15
+ result[key] = parseValue(stripped.slice(eqIndex + 1));
16
+ }
17
+ return result;
18
+ }
19
+ function parseValue(raw) {
20
+ const trimmed = raw.trim();
21
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
22
+ return trimmed.slice(1, -1);
23
+ }
24
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
25
+ return trimmed.slice(1, -1).replace(/\\([nrt"\\])/g, (_, ch) => {
26
+ const map = { n: "\n", r: "\r", t: " ", '"': '"', "\\": "\\" };
27
+ return map[ch] ?? ch;
28
+ });
29
+ }
30
+ const commentIndex = trimmed.indexOf(" #");
31
+ if (commentIndex !== -1) {
32
+ return trimmed.slice(0, commentIndex).trim();
33
+ }
34
+ return trimmed;
35
+ }
36
+
37
+ // src/core/validator.ts
38
+ function validate(schema, parsed) {
39
+ const errors = [];
40
+ const env = {};
41
+ for (const [key, field] of Object.entries(schema)) {
42
+ const raw = parsed[key];
43
+ if (raw === void 0) {
44
+ if (field._default !== void 0) {
45
+ env[key] = field._default;
46
+ continue;
47
+ }
48
+ if (field._required) {
49
+ errors.push({ key, message: "required but missing" });
50
+ continue;
51
+ }
52
+ env[key] = void 0;
53
+ continue;
54
+ }
55
+ const result = field._validate(raw);
56
+ if (result.success) {
57
+ env[key] = result.value;
58
+ } else {
59
+ errors.push({ key, message: result.message, received: raw });
60
+ }
61
+ }
62
+ if (errors.length > 0) return { ok: false, errors };
63
+ return { ok: true, env };
64
+ }
65
+
66
+ // src/core/errors.ts
67
+ var EnvValidationError = class extends Error {
68
+ errors;
69
+ constructor(errors) {
70
+ super(`envguard: ${errors.length} problem${errors.length > 1 ? "s" : ""} found`);
71
+ this.name = "EnvValidationError";
72
+ this.errors = errors;
73
+ }
74
+ };
75
+
76
+ // src/core/schema.ts
77
+ import { z } from "zod/v4";
78
+ function createField(opts, validate2) {
79
+ return {
80
+ _type: void 0,
81
+ _required: opts.default !== void 0 ? false : opts.required ?? true,
82
+ _default: opts.default,
83
+ _secret: opts.secret ?? false,
84
+ _parse(raw) {
85
+ const r = validate2(raw);
86
+ if (!r.success) throw new Error(r.message);
87
+ return r.value;
88
+ },
89
+ _validate: validate2
90
+ };
91
+ }
92
+ var _emailSchema = null;
93
+ var t = {
94
+ string(opts = {}) {
95
+ return createField(
96
+ opts,
97
+ (raw) => raw.length > 0 ? { success: true, value: raw } : { success: false, message: "expected non-empty string" }
98
+ );
99
+ },
100
+ number(opts = {}) {
101
+ return createField(opts, (raw) => {
102
+ const n = Number(raw);
103
+ return !isNaN(n) && raw.trim() !== "" ? { success: true, value: n } : { success: false, message: `"${raw}" is not a valid number` };
104
+ });
105
+ },
106
+ boolean(opts = {}) {
107
+ const truthy = /* @__PURE__ */ new Set(["true", "1", "yes"]);
108
+ const falsy = /* @__PURE__ */ new Set(["false", "0", "no"]);
109
+ return createField(opts, (raw) => {
110
+ const v = raw.toLowerCase();
111
+ if (truthy.has(v)) return { success: true, value: true };
112
+ if (falsy.has(v)) return { success: true, value: false };
113
+ return { success: false, message: `expected true/false, 1/0, or yes/no \u2014 got "${raw}"` };
114
+ });
115
+ },
116
+ port(opts = {}) {
117
+ return createField(opts, (raw) => {
118
+ const n = Number(raw);
119
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
120
+ return { success: false, message: `"${raw}" is not a valid port (expected 1-65535)` };
121
+ }
122
+ return { success: true, value: n };
123
+ });
124
+ },
125
+ url(opts = {}) {
126
+ return createField(opts, (raw) => {
127
+ try {
128
+ new URL(raw);
129
+ return { success: true, value: raw };
130
+ } catch {
131
+ return { success: false, message: `invalid URL: "${raw}"` };
132
+ }
133
+ });
134
+ },
135
+ email(opts = {}) {
136
+ return createField(opts, (raw) => {
137
+ _emailSchema ??= z.string().email();
138
+ return _emailSchema.safeParse(raw).success ? { success: true, value: raw } : { success: false, message: `invalid email: "${raw}"` };
139
+ });
140
+ },
141
+ enum(values, opts = {}) {
142
+ return createField(
143
+ opts,
144
+ (raw) => values.includes(raw) ? { success: true, value: raw } : { success: false, message: `"${raw}" is not one of: ${values.join(", ")}` }
145
+ );
146
+ },
147
+ json(opts = {}) {
148
+ return createField(opts, (raw) => {
149
+ try {
150
+ return { success: true, value: JSON.parse(raw) };
151
+ } catch {
152
+ return { success: false, message: `invalid JSON` };
153
+ }
154
+ });
155
+ }
156
+ };
157
+
158
+ // src/index.ts
159
+ function formatErrors(errors) {
160
+ const lines = errors.map((e) => ` ${e.key}: ${e.message}${e.received !== void 0 ? ` (got: ${e.received})` : ""}`);
161
+ return `envguard validation failed:
162
+ ${lines.join("\n")}`;
163
+ }
164
+ function envguard(schema, opts = {}) {
165
+ const { path, onError = "exit" } = opts;
166
+ const raw = path ? parse(readFileSync(path, "utf8")) : process.env;
167
+ const result = validate(schema, raw);
168
+ if (!result.ok) {
169
+ const msg = formatErrors(result.errors);
170
+ if (onError === "throw") throw new EnvValidationError(result.errors);
171
+ if (onError === "warn") {
172
+ console.warn(msg);
173
+ return {};
174
+ }
175
+ console.error(msg);
176
+ process.exit(1);
177
+ }
178
+ return result.env;
179
+ }
180
+ export {
181
+ EnvValidationError,
182
+ envguard,
183
+ t
184
+ };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@bladedev/envguard",
3
+ "version": "0.1.0",
4
+ "description": "Type-safe .env manager — schema, validation, sync, CLI",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "bin": {
22
+ "envguard": "./dist/cli/index.js"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "typecheck": "tsc --noEmit"
32
+ },
33
+ "keywords": [
34
+ "env",
35
+ "dotenv",
36
+ "validation",
37
+ "typescript",
38
+ "schema",
39
+ "cli"
40
+ ],
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/bladedevoff/envguard"
44
+ },
45
+ "license": "MIT",
46
+ "engines": {
47
+ "node": ">=18"
48
+ },
49
+ "dependencies": {
50
+ "citty": "^0.2.1",
51
+ "jiti": "^2.6.1",
52
+ "zod": "^4.3.6"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^25.5.0",
56
+ "tsup": "^8.5.1",
57
+ "tsx": "^4.21.0",
58
+ "typescript": "^6.0.2",
59
+ "vitest": "^4.1.1"
60
+ }
61
+ }