ultraenv 1.0.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 +21 -0
- package/README.md +2058 -0
- package/bin/ultraenv.mjs +3 -0
- package/dist/chunk-2USZPWLZ.js +288 -0
- package/dist/chunk-3UV2QNJL.js +270 -0
- package/dist/chunk-3VYXPTYV.js +179 -0
- package/dist/chunk-4XUYMRK5.js +366 -0
- package/dist/chunk-5G2DU52U.js +189 -0
- package/dist/chunk-6KS56D6E.js +172 -0
- package/dist/chunk-AWN6ADV7.js +328 -0
- package/dist/chunk-CHVO6NWI.js +203 -0
- package/dist/chunk-CIFMBJ4H.js +3975 -0
- package/dist/chunk-GC7RXHLA.js +253 -0
- package/dist/chunk-HFXQGJY3.js +445 -0
- package/dist/chunk-IGFVP24Q.js +91 -0
- package/dist/chunk-IKPTKALB.js +78 -0
- package/dist/chunk-JB7RKV3C.js +66 -0
- package/dist/chunk-MNVFG7H4.js +611 -0
- package/dist/chunk-MSXMESFP.js +1910 -0
- package/dist/chunk-N5PAV4NM.js +127 -0
- package/dist/chunk-NBOABPHM.js +158 -0
- package/dist/chunk-OMAOROL4.js +49 -0
- package/dist/chunk-R7PZRSZ7.js +105 -0
- package/dist/chunk-TE7HPLA6.js +73 -0
- package/dist/chunk-TMT5KCO3.js +101 -0
- package/dist/chunk-UEWYFN6A.js +189 -0
- package/dist/chunk-WMHN5RW2.js +128 -0
- package/dist/chunk-XC65ORJ5.js +70 -0
- package/dist/chunk-YMMP4VQL.js +118 -0
- package/dist/chunk-YN2KGTCB.js +33 -0
- package/dist/chunk-YTICOB5M.js +65 -0
- package/dist/chunk-YVWLXFUT.js +107 -0
- package/dist/ci-check-sync-VBMSVWIV.js +48 -0
- package/dist/ci-scan-24MT5XGS.js +41 -0
- package/dist/ci-setup-C2NKEFRD.js +135 -0
- package/dist/ci-validate-7AW24LSQ.js +57 -0
- package/dist/cli/index.cjs +9217 -0
- package/dist/cli/index.d.cts +9 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +339 -0
- package/dist/comparator-RDKX3OI7.js +13 -0
- package/dist/completion-MW35C2XO.js +168 -0
- package/dist/config-O5YRQP5Z.js +13 -0
- package/dist/debug-PTPXAF3K.js +131 -0
- package/dist/declaration-LEME4AFZ.js +10 -0
- package/dist/doctor-FZAUPKHS.js +129 -0
- package/dist/envs-compare-5K3HESX5.js +49 -0
- package/dist/envs-create-2XXHXMGA.js +58 -0
- package/dist/envs-list-NQM5252B.js +59 -0
- package/dist/envs-switch-6L2AQYID.js +50 -0
- package/dist/envs-validate-FL73Q76T.js +89 -0
- package/dist/fs-VH7ATUS3.js +31 -0
- package/dist/generator-LFZBMZZS.js +14 -0
- package/dist/git-BZS4DPAI.js +30 -0
- package/dist/help-3XJBXEHE.js +121 -0
- package/dist/index.cjs +12907 -0
- package/dist/index.d.cts +2562 -0
- package/dist/index.d.ts +2562 -0
- package/dist/index.js +3212 -0
- package/dist/init-Y7JQ2KYJ.js +146 -0
- package/dist/install-hook-SKXIV6NV.js +111 -0
- package/dist/json-schema-I26YNQBH.js +10 -0
- package/dist/key-manager-O3G55WPU.js +25 -0
- package/dist/middleware/express.cjs +103 -0
- package/dist/middleware/express.d.cts +115 -0
- package/dist/middleware/express.d.ts +115 -0
- package/dist/middleware/express.js +8 -0
- package/dist/middleware/fastify.cjs +91 -0
- package/dist/middleware/fastify.d.cts +111 -0
- package/dist/middleware/fastify.d.ts +111 -0
- package/dist/middleware/fastify.js +8 -0
- package/dist/module-IDIZPP4M.js +10 -0
- package/dist/protect-NCWPM6VC.js +161 -0
- package/dist/scan-TRLY36TT.js +58 -0
- package/dist/schema/index.cjs +4074 -0
- package/dist/schema/index.d.cts +1244 -0
- package/dist/schema/index.d.ts +1244 -0
- package/dist/schema/index.js +152 -0
- package/dist/sync-TMHMTLH2.js +186 -0
- package/dist/typegen-SQOSXBWM.js +80 -0
- package/dist/validate-IOAM5HWS.js +100 -0
- package/dist/vault-decrypt-U6HJZNBV.js +111 -0
- package/dist/vault-diff-B3ZOQTWI.js +132 -0
- package/dist/vault-encrypt-GUSLCSKS.js +112 -0
- package/dist/vault-init-GUBOTOUL.js +106 -0
- package/dist/vault-rekey-DAHT7JCN.js +132 -0
- package/dist/vault-status-GDLRU2OK.js +90 -0
- package/dist/vault-verify-CD76FJSF.js +102 -0
- package/package.json +106 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var UltraenvError = class extends Error {
|
|
3
|
+
/** Machine-readable error code (e.g., 'PARSE_ERROR', 'ENCRYPTION_ERROR') */
|
|
4
|
+
code;
|
|
5
|
+
/** Human-readable hint for how to fix this error */
|
|
6
|
+
hint;
|
|
7
|
+
/** The original cause of this error, if wrapping another error */
|
|
8
|
+
cause;
|
|
9
|
+
constructor(message, options = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = this.constructor.name;
|
|
12
|
+
this.code = options.code ?? "ULTRAENV_ERROR";
|
|
13
|
+
this.hint = options.hint;
|
|
14
|
+
this.cause = options.cause;
|
|
15
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
16
|
+
}
|
|
17
|
+
/** Return a formatted string with code, message, and hint */
|
|
18
|
+
toString() {
|
|
19
|
+
const parts = [`[${this.code}] ${this.message}`];
|
|
20
|
+
if (this.hint !== void 0) {
|
|
21
|
+
parts.push(` Hint: ${this.hint}`);
|
|
22
|
+
}
|
|
23
|
+
return parts.join("\n");
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var ValidationError = class extends UltraenvError {
|
|
27
|
+
/** The variable name that failed validation */
|
|
28
|
+
field;
|
|
29
|
+
/** The actual value that was provided */
|
|
30
|
+
value;
|
|
31
|
+
/** The schema definition that was violated */
|
|
32
|
+
schema;
|
|
33
|
+
/** Expected type or constraint description */
|
|
34
|
+
expected;
|
|
35
|
+
constructor(field, value, message, options = {}) {
|
|
36
|
+
const fullMessage = message || `Validation failed for "${field}"`;
|
|
37
|
+
super(fullMessage, {
|
|
38
|
+
code: "VALIDATION_ERROR",
|
|
39
|
+
hint: options.hint ?? `Check the value of "${field}" in your .env file. Expected: ${options.expected ?? "see schema definition"}.`,
|
|
40
|
+
cause: options.cause
|
|
41
|
+
});
|
|
42
|
+
this.field = field;
|
|
43
|
+
this.value = value;
|
|
44
|
+
this.schema = options.schema;
|
|
45
|
+
this.expected = options.expected ?? "valid value";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var ParseError = class extends UltraenvError {
|
|
49
|
+
/** 1-based line number where the error occurred */
|
|
50
|
+
line;
|
|
51
|
+
/** 1-based column number where the error occurred */
|
|
52
|
+
column;
|
|
53
|
+
/** The raw content of the line that caused the error */
|
|
54
|
+
raw;
|
|
55
|
+
/** Path to the file being parsed */
|
|
56
|
+
filePath;
|
|
57
|
+
constructor(message, options = {}) {
|
|
58
|
+
const location = options.line !== void 0 ? ` at line ${options.line}${options.column !== void 0 ? `, column ${options.column}` : ""}` : "";
|
|
59
|
+
const file = options.filePath !== void 0 ? ` in "${options.filePath}"` : "";
|
|
60
|
+
const fullMessage = `${message}${file}${location}`;
|
|
61
|
+
super(fullMessage, {
|
|
62
|
+
code: "PARSE_ERROR",
|
|
63
|
+
hint: options.hint ?? "Check your .env file for invalid syntax. Each line should be in the format KEY=VALUE or # comment.",
|
|
64
|
+
cause: options.cause
|
|
65
|
+
});
|
|
66
|
+
this.line = options.line ?? 0;
|
|
67
|
+
this.column = options.column ?? 0;
|
|
68
|
+
this.raw = options.raw ?? "";
|
|
69
|
+
this.filePath = options.filePath ?? "";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var InterpolationError = class extends UltraenvError {
|
|
73
|
+
/** The variable reference that caused the error */
|
|
74
|
+
variable;
|
|
75
|
+
/** Whether this was caused by a circular reference */
|
|
76
|
+
circular;
|
|
77
|
+
constructor(message, options = {}) {
|
|
78
|
+
super(message, {
|
|
79
|
+
code: "INTERPOLATION_ERROR",
|
|
80
|
+
hint: options.hint ?? (options.circular ? "Circular variable references are not allowed. Check that no two variables reference each other." : `Make sure "${options.variable ?? "the referenced variable"}" is defined before it is referenced.`),
|
|
81
|
+
cause: options.cause
|
|
82
|
+
});
|
|
83
|
+
this.variable = options.variable ?? "";
|
|
84
|
+
this.circular = options.circular ?? false;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var EncryptionError = class extends UltraenvError {
|
|
88
|
+
constructor(message, options = {}) {
|
|
89
|
+
super(message, {
|
|
90
|
+
code: "ENCRYPTION_ERROR",
|
|
91
|
+
hint: options.hint ?? 'Ensure your encryption key is valid and has not been corrupted. Try regenerating the key with "ultraenv key generate".',
|
|
92
|
+
cause: options.cause
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var VaultError = class extends UltraenvError {
|
|
97
|
+
/** The environment name involved */
|
|
98
|
+
environment;
|
|
99
|
+
/** The vault operation that failed */
|
|
100
|
+
operation;
|
|
101
|
+
constructor(message, options = {}) {
|
|
102
|
+
super(message, {
|
|
103
|
+
code: "VAULT_ERROR",
|
|
104
|
+
hint: options.hint ?? `Check that the vault file exists and is properly formatted for environment "${options.environment ?? "unknown"}". Run "ultraenv vault status" for diagnostics.`,
|
|
105
|
+
cause: options.cause
|
|
106
|
+
});
|
|
107
|
+
this.environment = options.environment ?? "";
|
|
108
|
+
this.operation = options.operation ?? "";
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var ScanError = class extends UltraenvError {
|
|
112
|
+
/** The file being scanned */
|
|
113
|
+
file;
|
|
114
|
+
/** The line number where the error occurred */
|
|
115
|
+
line;
|
|
116
|
+
/** The secret pattern that triggered the error */
|
|
117
|
+
pattern;
|
|
118
|
+
constructor(message, options = {}) {
|
|
119
|
+
super(message, {
|
|
120
|
+
code: "SCAN_ERROR",
|
|
121
|
+
hint: options.hint ?? "Check that all files being scanned are accessible and contain valid text content.",
|
|
122
|
+
cause: options.cause
|
|
123
|
+
});
|
|
124
|
+
this.file = options.file ?? "";
|
|
125
|
+
this.line = options.line ?? 0;
|
|
126
|
+
this.pattern = options.pattern ?? "";
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var ConfigError = class extends UltraenvError {
|
|
130
|
+
/** The configuration field that is invalid */
|
|
131
|
+
field;
|
|
132
|
+
constructor(message, options = {}) {
|
|
133
|
+
super(message, {
|
|
134
|
+
code: "CONFIG_ERROR",
|
|
135
|
+
hint: options.hint ?? `Check your ultraenv configuration. The field "${options.field ?? "unknown"}" may have an invalid value or type.`,
|
|
136
|
+
cause: options.cause
|
|
137
|
+
});
|
|
138
|
+
this.field = options.field ?? "";
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
var FileSystemError = class extends UltraenvError {
|
|
142
|
+
/** The file or directory path involved */
|
|
143
|
+
path;
|
|
144
|
+
/** The operation that failed (e.g., 'read', 'write', 'mkdir') */
|
|
145
|
+
operation;
|
|
146
|
+
/** The underlying system error code (e.g., 'ENOENT', 'EACCES') */
|
|
147
|
+
code;
|
|
148
|
+
constructor(message, options = {}) {
|
|
149
|
+
const fsCode = options.code ?? options.cause?.code ?? "UNKNOWN";
|
|
150
|
+
const opHint = options.path !== void 0 ? `Could not ${options.operation ?? "access"} "${options.path}".` : "";
|
|
151
|
+
super(message, {
|
|
152
|
+
code: `FS_${fsCode}`,
|
|
153
|
+
hint: options.hint ?? `${opHint} Ensure the file exists and you have the necessary permissions.`,
|
|
154
|
+
cause: options.cause
|
|
155
|
+
});
|
|
156
|
+
this.path = options.path ?? "";
|
|
157
|
+
this.operation = options.operation ?? "";
|
|
158
|
+
this.code = fsCode;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
function isUltraenvError(error) {
|
|
162
|
+
return error instanceof UltraenvError;
|
|
163
|
+
}
|
|
164
|
+
function getErrorMessage(error) {
|
|
165
|
+
if (error instanceof UltraenvError) {
|
|
166
|
+
return error.toString();
|
|
167
|
+
}
|
|
168
|
+
if (error instanceof Error) {
|
|
169
|
+
return error.message;
|
|
170
|
+
}
|
|
171
|
+
if (typeof error === "string") {
|
|
172
|
+
return error;
|
|
173
|
+
}
|
|
174
|
+
return `Unknown error: ${String(error)}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export {
|
|
178
|
+
UltraenvError,
|
|
179
|
+
ValidationError,
|
|
180
|
+
ParseError,
|
|
181
|
+
InterpolationError,
|
|
182
|
+
EncryptionError,
|
|
183
|
+
VaultError,
|
|
184
|
+
ScanError,
|
|
185
|
+
ConfigError,
|
|
186
|
+
FileSystemError,
|
|
187
|
+
isUltraenvError,
|
|
188
|
+
getErrorMessage
|
|
189
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
writeFile
|
|
3
|
+
} from "./chunk-3VYXPTYV.js";
|
|
4
|
+
|
|
5
|
+
// src/typegen/json-schema.ts
|
|
6
|
+
var JSON_SCHEMA_DRAFT_07 = "http://json-schema.org/draft-07/schema#";
|
|
7
|
+
function schemaToJsonSchemaProperty(key, schema, includeDescriptions) {
|
|
8
|
+
const r = schema;
|
|
9
|
+
const schemaType = r.type;
|
|
10
|
+
const property = {
|
|
11
|
+
type: "string"
|
|
12
|
+
};
|
|
13
|
+
if (includeDescriptions && schema.description !== void 0) {
|
|
14
|
+
property.description = schema.description;
|
|
15
|
+
} else if (includeDescriptions) {
|
|
16
|
+
property.description = key;
|
|
17
|
+
}
|
|
18
|
+
if (schema.default !== void 0) {
|
|
19
|
+
property.default = schema.default;
|
|
20
|
+
} else if (r.hasDefault === true && typeof r.rawDefaultValue === "string") {
|
|
21
|
+
const raw = r.rawDefaultValue;
|
|
22
|
+
if (raw === "true") {
|
|
23
|
+
property.default = true;
|
|
24
|
+
} else if (raw === "false") {
|
|
25
|
+
property.default = false;
|
|
26
|
+
} else if (raw !== "" && !Number.isNaN(Number(raw)) && raw === String(Number(raw))) {
|
|
27
|
+
property.default = Number(raw);
|
|
28
|
+
} else {
|
|
29
|
+
property.default = raw;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
switch (schemaType) {
|
|
33
|
+
case "string": {
|
|
34
|
+
property.type = "string";
|
|
35
|
+
const enumValues = r.enum;
|
|
36
|
+
if (Array.isArray(enumValues) && enumValues.length > 0) {
|
|
37
|
+
property.enum = [...enumValues];
|
|
38
|
+
}
|
|
39
|
+
if (typeof r.minLength === "number") {
|
|
40
|
+
property.minLength = r.minLength;
|
|
41
|
+
}
|
|
42
|
+
if (typeof r.maxLength === "number") {
|
|
43
|
+
property.maxLength = r.maxLength;
|
|
44
|
+
}
|
|
45
|
+
if (r.pattern instanceof RegExp) {
|
|
46
|
+
property.pattern = r.pattern.source;
|
|
47
|
+
}
|
|
48
|
+
if (typeof r.format === "string") {
|
|
49
|
+
property.format = mapFormat(r.format);
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "number": {
|
|
54
|
+
property.type = r.integer === true ? "integer" : "number";
|
|
55
|
+
if (typeof r.min === "number") {
|
|
56
|
+
property.minimum = r.min;
|
|
57
|
+
if (r.positive === true) {
|
|
58
|
+
property.exclusiveMinimum = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (typeof r.max === "number") {
|
|
62
|
+
property.maximum = r.max;
|
|
63
|
+
if (r.negative === true) {
|
|
64
|
+
property.exclusiveMaximum = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "boolean": {
|
|
70
|
+
property.type = "boolean";
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "enum": {
|
|
74
|
+
property.type = "string";
|
|
75
|
+
const values = r.values;
|
|
76
|
+
if (Array.isArray(values) && values.length > 0) {
|
|
77
|
+
property.enum = [...values];
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case "array": {
|
|
82
|
+
property.type = "array";
|
|
83
|
+
property.items = { type: "string" };
|
|
84
|
+
if (typeof r.minItems === "number") {
|
|
85
|
+
property.minItems = r.minItems;
|
|
86
|
+
}
|
|
87
|
+
if (typeof r.maxItems === "number") {
|
|
88
|
+
property.maxItems = r.maxItems;
|
|
89
|
+
}
|
|
90
|
+
if (r.unique === true) {
|
|
91
|
+
property.uniqueItems = true;
|
|
92
|
+
}
|
|
93
|
+
if (typeof r.separator === "string" && r.separator !== ",") {
|
|
94
|
+
property.description = (property.description ?? "") + ` (separator: "${r.separator}")`;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case "json": {
|
|
99
|
+
property.type = "object";
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "date": {
|
|
103
|
+
property.type = "string";
|
|
104
|
+
property.format = "date-time";
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "bigint": {
|
|
108
|
+
property.type = "string";
|
|
109
|
+
property.description = (property.description ?? "") + " (bigint as string)";
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
default: {
|
|
113
|
+
property.type = "string";
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return property;
|
|
118
|
+
}
|
|
119
|
+
function mapFormat(format) {
|
|
120
|
+
const formatMap = /* @__PURE__ */ new Map([
|
|
121
|
+
["email", "email"],
|
|
122
|
+
["url", "uri"],
|
|
123
|
+
["uuid", "uuid"],
|
|
124
|
+
["hostname", "hostname"],
|
|
125
|
+
["ip", "string"],
|
|
126
|
+
["ipv4", "ipv4"],
|
|
127
|
+
["ipv6", "ipv6"]
|
|
128
|
+
]);
|
|
129
|
+
return formatMap.get(format) ?? "string";
|
|
130
|
+
}
|
|
131
|
+
async function generateJsonSchema(schema, outputPath, options) {
|
|
132
|
+
const content = generateJsonSchemaContent(schema, options);
|
|
133
|
+
if (outputPath !== void 0) {
|
|
134
|
+
await writeFile(outputPath, content);
|
|
135
|
+
}
|
|
136
|
+
return content;
|
|
137
|
+
}
|
|
138
|
+
function generateJsonSchemaContent(schema, options) {
|
|
139
|
+
const includeDescriptions = options?.includeDescriptions ?? true;
|
|
140
|
+
const indent = options?.indent ?? 2;
|
|
141
|
+
const title = options?.title ?? "Environment Variables";
|
|
142
|
+
const description = options?.description ?? "Schema for environment variables managed by ultraenv";
|
|
143
|
+
const properties = {};
|
|
144
|
+
const required = [];
|
|
145
|
+
const sortedKeys = Object.keys(schema).sort();
|
|
146
|
+
for (const key of sortedKeys) {
|
|
147
|
+
const schemaEntry = schema[key];
|
|
148
|
+
if (schemaEntry === void 0) continue;
|
|
149
|
+
properties[key] = schemaToJsonSchemaProperty(key, schemaEntry, includeDescriptions);
|
|
150
|
+
const r = schemaEntry;
|
|
151
|
+
const isOptional = schemaEntry.optional === true || schemaEntry.default !== void 0 || r.hasDefault === true;
|
|
152
|
+
if (!isOptional) {
|
|
153
|
+
required.push(key);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
required.sort();
|
|
157
|
+
const document = {
|
|
158
|
+
$schema: JSON_SCHEMA_DRAFT_07,
|
|
159
|
+
title,
|
|
160
|
+
description,
|
|
161
|
+
type: "object",
|
|
162
|
+
properties,
|
|
163
|
+
required,
|
|
164
|
+
additionalProperties: false
|
|
165
|
+
};
|
|
166
|
+
return JSON.stringify(document, null, indent) + "\n";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export {
|
|
170
|
+
generateJsonSchema,
|
|
171
|
+
generateJsonSchemaContent
|
|
172
|
+
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseEnvFile
|
|
3
|
+
} from "./chunk-HFXQGJY3.js";
|
|
4
|
+
import {
|
|
5
|
+
exists,
|
|
6
|
+
readFile,
|
|
7
|
+
removeFile,
|
|
8
|
+
writeFile
|
|
9
|
+
} from "./chunk-3VYXPTYV.js";
|
|
10
|
+
import {
|
|
11
|
+
FileSystemError
|
|
12
|
+
} from "./chunk-5G2DU52U.js";
|
|
13
|
+
|
|
14
|
+
// src/environments/creator.ts
|
|
15
|
+
import { resolve, join } from "path";
|
|
16
|
+
var NODEJS_TEMPLATE = `# Node.js Backend Environment
|
|
17
|
+
# Created by ultraenv
|
|
18
|
+
|
|
19
|
+
# Server
|
|
20
|
+
PORT=3000
|
|
21
|
+
HOST=localhost
|
|
22
|
+
NODE_ENV=development
|
|
23
|
+
|
|
24
|
+
# Database
|
|
25
|
+
DATABASE_URL=
|
|
26
|
+
DATABASE_POOL_SIZE=10
|
|
27
|
+
|
|
28
|
+
# Authentication
|
|
29
|
+
JWT_SECRET=
|
|
30
|
+
JWT_EXPIRES_IN=7d
|
|
31
|
+
|
|
32
|
+
# CORS
|
|
33
|
+
ALLOWED_ORIGINS=http://localhost:3000
|
|
34
|
+
|
|
35
|
+
# Logging
|
|
36
|
+
LOG_LEVEL=info
|
|
37
|
+
LOG_FORMAT=json
|
|
38
|
+
|
|
39
|
+
# Feature Flags
|
|
40
|
+
DEBUG=false
|
|
41
|
+
VERBOSE=false
|
|
42
|
+
`;
|
|
43
|
+
var NEXTJS_TEMPLATE = `# Next.js Frontend Environment
|
|
44
|
+
# Created by ultraenv
|
|
45
|
+
|
|
46
|
+
# Framework
|
|
47
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
48
|
+
NEXT_PUBLIC_API_URL=http://localhost:3001
|
|
49
|
+
|
|
50
|
+
# Analytics
|
|
51
|
+
NEXT_PUBLIC_GA_ID=
|
|
52
|
+
NEXT_PUBLIC_SENTRY_DSN=
|
|
53
|
+
|
|
54
|
+
# Feature Flags
|
|
55
|
+
NEXT_PUBLIC_DEBUG=false
|
|
56
|
+
NEXT_PUBLIC_MAINTENANCE=false
|
|
57
|
+
`;
|
|
58
|
+
var DOCKER_TEMPLATE = `# Docker Deployment Environment
|
|
59
|
+
# Created by ultraenv
|
|
60
|
+
|
|
61
|
+
# Container
|
|
62
|
+
CONTAINER_PORT=3000
|
|
63
|
+
HOST_PORT=3000
|
|
64
|
+
|
|
65
|
+
# Database
|
|
66
|
+
POSTGRES_HOST=localhost
|
|
67
|
+
POSTGRES_PORT=5432
|
|
68
|
+
POSTGRES_USER=
|
|
69
|
+
POSTGRES_PASSWORD=
|
|
70
|
+
POSTGRES_DB=
|
|
71
|
+
|
|
72
|
+
# Redis
|
|
73
|
+
REDIS_HOST=localhost
|
|
74
|
+
REDIS_PORT=6379
|
|
75
|
+
REDIS_PASSWORD=
|
|
76
|
+
|
|
77
|
+
# Networking
|
|
78
|
+
NETWORK_NAME=app-network
|
|
79
|
+
`;
|
|
80
|
+
var BUILTIN_TEMPLATES = /* @__PURE__ */ new Map([
|
|
81
|
+
["nodejs", NODEJS_TEMPLATE],
|
|
82
|
+
["nextjs", NEXTJS_TEMPLATE],
|
|
83
|
+
["docker", DOCKER_TEMPLATE]
|
|
84
|
+
]);
|
|
85
|
+
function extractVariables(content, schema) {
|
|
86
|
+
const parsed = parseEnvFile(content);
|
|
87
|
+
const variables = [];
|
|
88
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
89
|
+
if (schema !== void 0) {
|
|
90
|
+
for (const [key, schemaEntry] of Object.entries(schema)) {
|
|
91
|
+
if (seenKeys.has(key)) continue;
|
|
92
|
+
seenKeys.add(key);
|
|
93
|
+
const r = schemaEntry;
|
|
94
|
+
variables.push({
|
|
95
|
+
key,
|
|
96
|
+
value: "",
|
|
97
|
+
description: schemaEntry.description,
|
|
98
|
+
isSecret: r.isSecret === true,
|
|
99
|
+
isRequired: schemaEntry.optional !== true && schemaEntry.default === void 0 && r.hasDefault !== true,
|
|
100
|
+
hasDefault: schemaEntry.default !== void 0 || r.hasDefault === true,
|
|
101
|
+
defaultValue: typeof r.rawDefaultValue === "string" ? r.rawDefaultValue : void 0,
|
|
102
|
+
type: typeof r.typeName === "string" ? r.typeName : "string"
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const envVar of parsed.vars) {
|
|
107
|
+
if (seenKeys.has(envVar.key)) continue;
|
|
108
|
+
seenKeys.add(envVar.key);
|
|
109
|
+
variables.push({
|
|
110
|
+
key: envVar.key,
|
|
111
|
+
value: envVar.value,
|
|
112
|
+
isSecret: false,
|
|
113
|
+
isRequired: false,
|
|
114
|
+
hasDefault: false,
|
|
115
|
+
type: "string"
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return variables;
|
|
119
|
+
}
|
|
120
|
+
function generateEnvironmentContent(variables, values) {
|
|
121
|
+
const lines = [];
|
|
122
|
+
lines.push(`# Environment: generated by ultraenv`);
|
|
123
|
+
lines.push(`# Created: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
124
|
+
lines.push("");
|
|
125
|
+
for (const variable of variables) {
|
|
126
|
+
if (variable.description !== void 0) {
|
|
127
|
+
lines.push(`# ${variable.description}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push(`# type: ${variable.type}`);
|
|
130
|
+
if (variable.isRequired) {
|
|
131
|
+
lines.push("# required");
|
|
132
|
+
} else if (variable.hasDefault && variable.defaultValue !== void 0) {
|
|
133
|
+
lines.push(`# default: ${variable.defaultValue}`);
|
|
134
|
+
} else {
|
|
135
|
+
lines.push("# optional");
|
|
136
|
+
}
|
|
137
|
+
if (variable.isSecret) {
|
|
138
|
+
lines.push("# [SECRET]");
|
|
139
|
+
}
|
|
140
|
+
const providedValue = values[variable.key];
|
|
141
|
+
if (providedValue !== void 0) {
|
|
142
|
+
lines.push(`${variable.key}=${providedValue}`);
|
|
143
|
+
} else if (variable.hasDefault && variable.defaultValue !== void 0) {
|
|
144
|
+
lines.push(`${variable.key}=${variable.defaultValue}`);
|
|
145
|
+
} else if (variable.isSecret) {
|
|
146
|
+
lines.push(`${variable.key}=`);
|
|
147
|
+
} else if (variable.isRequired) {
|
|
148
|
+
lines.push(`${variable.key}=`);
|
|
149
|
+
} else {
|
|
150
|
+
lines.push(`# ${variable.key}=`);
|
|
151
|
+
}
|
|
152
|
+
lines.push("");
|
|
153
|
+
}
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
function collectValues(variables, providedValues) {
|
|
157
|
+
const values = { ...providedValues };
|
|
158
|
+
for (const variable of variables) {
|
|
159
|
+
if (variable.key in values) continue;
|
|
160
|
+
if (!variable.isRequired && !variable.hasDefault) continue;
|
|
161
|
+
if (variable.hasDefault && variable.defaultValue !== void 0) {
|
|
162
|
+
values[variable.key] = variable.defaultValue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return values;
|
|
166
|
+
}
|
|
167
|
+
function validateEnvironmentName(name) {
|
|
168
|
+
if (name.length === 0) {
|
|
169
|
+
throw new FileSystemError("Environment name cannot be empty", {
|
|
170
|
+
path: "",
|
|
171
|
+
operation: "validate",
|
|
172
|
+
hint: 'Provide a valid environment name (e.g., "staging", "production").'
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (name.length > 64) {
|
|
176
|
+
throw new FileSystemError(
|
|
177
|
+
`Environment name too long: ${name.length} characters (max 64)`,
|
|
178
|
+
{
|
|
179
|
+
path: "",
|
|
180
|
+
operation: "validate",
|
|
181
|
+
hint: "Use a shorter environment name."
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
186
|
+
throw new FileSystemError(
|
|
187
|
+
`Invalid environment name: "${name}". Only alphanumeric characters, hyphens, and underscores are allowed.`,
|
|
188
|
+
{
|
|
189
|
+
path: "",
|
|
190
|
+
operation: "validate",
|
|
191
|
+
hint: 'Use names like "staging", "production", "ci", "dev-v2", etc.'
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const reserved = ["local", "example", "template", "bak", "backup", "old", "tmp", "temp"];
|
|
196
|
+
if (reserved.includes(name.toLowerCase())) {
|
|
197
|
+
throw new FileSystemError(
|
|
198
|
+
`Reserved environment name: "${name}"`,
|
|
199
|
+
{
|
|
200
|
+
path: "",
|
|
201
|
+
operation: "validate",
|
|
202
|
+
hint: `The name "${name}" is reserved. Choose a different name.`
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function createEnvironment(name, options) {
|
|
208
|
+
validateEnvironmentName(name);
|
|
209
|
+
const baseDir = resolve(options?.cwd ?? process.cwd());
|
|
210
|
+
const outputPath = join(baseDir, `.env.${name}`);
|
|
211
|
+
if (await exists(outputPath)) {
|
|
212
|
+
throw new FileSystemError(
|
|
213
|
+
`Environment file already exists: ".env.${name}"`,
|
|
214
|
+
{
|
|
215
|
+
path: outputPath,
|
|
216
|
+
operation: "create",
|
|
217
|
+
hint: "Delete the existing file first, or use a different environment name."
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
let content;
|
|
222
|
+
let variables = [];
|
|
223
|
+
if (options?.copyFrom !== void 0) {
|
|
224
|
+
const sourcePath = join(baseDir, `.env.${options.copyFrom}`);
|
|
225
|
+
if (!await exists(sourcePath)) {
|
|
226
|
+
throw new FileSystemError(
|
|
227
|
+
`Source environment not found: ".env.${options.copyFrom}"`,
|
|
228
|
+
{
|
|
229
|
+
path: sourcePath,
|
|
230
|
+
operation: "read",
|
|
231
|
+
hint: `Ensure ".env.${options.copyFrom}" exists before copying.`
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
content = await readFile(sourcePath);
|
|
236
|
+
variables = extractVariables(content, options?.schema);
|
|
237
|
+
const sourceParsed = parseEnvFile(content, sourcePath);
|
|
238
|
+
const sourceValues = {};
|
|
239
|
+
for (const envVar of sourceParsed.vars) {
|
|
240
|
+
sourceValues[envVar.key] = envVar.value;
|
|
241
|
+
}
|
|
242
|
+
content = generateEnvironmentContent(variables, {
|
|
243
|
+
...sourceValues,
|
|
244
|
+
...options?.values
|
|
245
|
+
});
|
|
246
|
+
} else if (options?.fromTemplate !== void 0) {
|
|
247
|
+
const templatePath = resolve(options.fromTemplate);
|
|
248
|
+
const builtinTemplate = BUILTIN_TEMPLATES.get(options.fromTemplate.toLowerCase());
|
|
249
|
+
if (builtinTemplate !== void 0) {
|
|
250
|
+
content = builtinTemplate;
|
|
251
|
+
} else if (await exists(templatePath)) {
|
|
252
|
+
content = await readFile(templatePath);
|
|
253
|
+
} else {
|
|
254
|
+
throw new FileSystemError(
|
|
255
|
+
`Template not found: "${options.fromTemplate}"`,
|
|
256
|
+
{
|
|
257
|
+
path: templatePath,
|
|
258
|
+
operation: "read",
|
|
259
|
+
hint: "Use a built-in template name (nodejs, nextjs, docker) or a valid file path."
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
variables = extractVariables(content, options?.schema);
|
|
264
|
+
const values = collectValues(variables, options?.values);
|
|
265
|
+
content = generateEnvironmentContent(variables, values);
|
|
266
|
+
} else if (options?.schema !== void 0) {
|
|
267
|
+
const schemaVars = [];
|
|
268
|
+
for (const [key, schemaEntry] of Object.entries(options.schema)) {
|
|
269
|
+
const r = schemaEntry;
|
|
270
|
+
schemaVars.push({
|
|
271
|
+
key,
|
|
272
|
+
value: "",
|
|
273
|
+
description: schemaEntry.description,
|
|
274
|
+
isSecret: r.isSecret === true,
|
|
275
|
+
isRequired: schemaEntry.optional !== true && schemaEntry.default === void 0 && r.hasDefault !== true,
|
|
276
|
+
hasDefault: schemaEntry.default !== void 0 || r.hasDefault === true,
|
|
277
|
+
defaultValue: typeof r.rawDefaultValue === "string" ? r.rawDefaultValue : void 0,
|
|
278
|
+
type: typeof r.typeName === "string" ? r.typeName : "string"
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const values = collectValues(schemaVars, options?.values);
|
|
282
|
+
content = generateEnvironmentContent(schemaVars, values);
|
|
283
|
+
} else {
|
|
284
|
+
content = [
|
|
285
|
+
`# Environment: ${name}`,
|
|
286
|
+
`# Created by ultraenv on ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
287
|
+
"",
|
|
288
|
+
"# Add your environment variables here",
|
|
289
|
+
""
|
|
290
|
+
].join("\n");
|
|
291
|
+
}
|
|
292
|
+
await writeFile(outputPath, content);
|
|
293
|
+
}
|
|
294
|
+
function listTemplates() {
|
|
295
|
+
return [
|
|
296
|
+
{ name: "nodejs", description: "Node.js backend environment (PORT, DATABASE_URL, JWT, CORS, etc.)" },
|
|
297
|
+
{ name: "nextjs", description: "Next.js frontend environment (NEXT_PUBLIC_* variables, analytics, etc.)" },
|
|
298
|
+
{ name: "docker", description: "Docker deployment environment (container ports, Postgres, Redis, etc.)" }
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
async function removeEnvironment(name, cwd) {
|
|
302
|
+
const baseDir = resolve(cwd ?? process.cwd());
|
|
303
|
+
const filePath = join(baseDir, `.env.${name}`);
|
|
304
|
+
if (!await exists(filePath)) {
|
|
305
|
+
throw new FileSystemError(
|
|
306
|
+
`Environment file not found: ".env.${name}"`,
|
|
307
|
+
{
|
|
308
|
+
path: filePath,
|
|
309
|
+
operation: "unlink",
|
|
310
|
+
hint: "The environment does not exist."
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
await removeFile(filePath);
|
|
315
|
+
}
|
|
316
|
+
async function duplicateEnvironment(sourceName, targetName, cwd) {
|
|
317
|
+
await createEnvironment(targetName, {
|
|
318
|
+
copyFrom: sourceName,
|
|
319
|
+
cwd
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export {
|
|
324
|
+
createEnvironment,
|
|
325
|
+
listTemplates,
|
|
326
|
+
removeEnvironment,
|
|
327
|
+
duplicateEnvironment
|
|
328
|
+
};
|