envelope-env 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.md ADDED
@@ -0,0 +1,13 @@
1
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
+ Version 2, December 2004
3
+
4
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
package/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # Envelope 📨
2
+
3
+ Envelope is a CLI tool that compiles a `.env` from a set of
4
+
5
+ ## Installation
6
+
7
+ ### Global Installation
8
+
9
+ ```bash
10
+ npm install -g envelope
11
+ ```
12
+
13
+ ### Local Development
14
+
15
+ ```bash
16
+ git clone <repository-url>
17
+ cd envelope-node
18
+ npm install
19
+ npm run build
20
+ ```
21
+
22
+ ## Project Structure
23
+
24
+ Envelope expects your project to have the following structure:
25
+
26
+ ```
27
+ your-project/
28
+ ├── env/ # Environment configuration directory
29
+ │ ├── .env # Common environment variables (shared across all envs)
30
+ │ ├── development/
31
+ │ │ └── .env # Development-specific variables
32
+ │ ├── staging/
33
+ │ │ └── .env # Staging-specific variables
34
+ │ ├── production/
35
+ │ │ └── .env # Production-specific variables
36
+ │ └── some-custom-env/
37
+ │ └── .env # Production-specific variables
38
+ ├── .env # Compiled by running `envelope use <environment name>`
39
+ └── ...
40
+ ```
41
+
42
+ Alternatively you can use the `.env.<name>` syntax:
43
+
44
+ ```
45
+ your-project/
46
+ ├── env/ # Environment configuration directory
47
+ │ ├── .env # Common environment variables (shared across all envs)
48
+ │ ├── .env.development # Development-specific variables
49
+ │ ├── .env.staging # Staging environment
50
+ │ ├── .env.production # Production environment
51
+ │ └── .env.some-custom-env # Production environment
52
+ ├── .env # Compiled by running `envelope use <environment name>`
53
+ └── ...
54
+ ```
55
+
56
+ ## Usage
57
+
58
+ ### Commands
59
+
60
+ #### `envelope list`
61
+
62
+ List all available environments in your project.
63
+
64
+ ```bash
65
+ envelope list
66
+ ```
67
+
68
+ **Output:**
69
+
70
+ ```
71
+ Available environments: development, staging, production
72
+ ```
73
+
74
+ #### `envelope get <environment>`
75
+
76
+ Display the compiled environment variables for a specific environment without writing to file.
77
+
78
+ ```bash
79
+ envelope get development
80
+ envelope get production --silent
81
+ ```
82
+
83
+ **Options:**
84
+
85
+ - `--silent, -s` - Suppress status messages
86
+
87
+ #### `envelope use <environment>`
88
+
89
+ Compile and write environment variables for a specific environment to your project's `.env` file.
90
+
91
+ ```bash
92
+ envelope use development
93
+ envelope use production --silent
94
+ ```
95
+
96
+ **Options:**
97
+
98
+ - `--silent, -s` - Suppress status messages
99
+
100
+ ### Examples
101
+
102
+ #### Setting up a new environment
103
+
104
+ 1. Create your environment directory:
105
+
106
+ ```bash
107
+ mkdir -p env/staging
108
+ ```
109
+
110
+ 2. Create environment-specific variables:
111
+
112
+ ```bash
113
+ # env/staging/.env
114
+ DATABASE_URL=postgresql://staging:pass@localhost:5432/staging_db
115
+ API_URL=https://staging-api.example.com
116
+ LOG_LEVEL=debug
117
+ ```
118
+
119
+ 3. Use the environment:
120
+
121
+ ```bash
122
+ envelope use staging
123
+ ```
124
+
125
+ #### Viewing environment variables
126
+
127
+ ```bash
128
+ # View development environment variables
129
+ envelope get development
130
+
131
+ # View production environment variables silently
132
+ envelope get production --silent
133
+ ```
134
+
135
+ ## Environment File Format
136
+
137
+ Environment files follow standard `.env` format:
138
+
139
+ ```bash
140
+ # Comments start with #
141
+ DATABASE_URL=postgresql://localhost:5432/mydb
142
+ API_KEY=your-secret-key
143
+ DEBUG=true
144
+
145
+ # Empty lines are allowed
146
+
147
+ # Variables can contain spaces and special characters
148
+ COMPLEX_VALUE="This is a complex value with spaces"
149
+ ```
150
+
151
+ ## Environment Variable Precedence
152
+
153
+ When compiling environment variables, Envelope follows this order:
154
+
155
+ 1. **Base variables** - `ENVELOPE_ENV` and `ENVELOPE_DIR` are always set
156
+ 2. **Common variables** - From `env/.env` (if it exists)
157
+ 3. **Environment-specific variables** - From `env/<environment>/.env`
158
+
159
+ Environment-specific variables will override common variables with the same name.
160
+
161
+ ## Development
162
+
163
+ ### Building
164
+
165
+ ```bash
166
+ npm run build
167
+ ```
168
+
169
+ ### Development Mode
170
+
171
+ ```bash
172
+ npm run dev
173
+ ```
174
+
175
+ ### Testing
176
+
177
+ ```bash
178
+ npm test
179
+ ```
180
+
181
+ ## Contributing
182
+
183
+ 1. Fork the repository
184
+ 2. Create a feature branch
185
+ 3. Make your changes
186
+ 4. Add tests if applicable
187
+ 5. Submit a pull request
188
+
189
+ ## License
190
+
191
+ WTFPL
package/dist/index.cjs ADDED
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/main.ts
27
+ var import_fs = __toESM(require("fs"), 1);
28
+ var import_path = __toESM(require("path"), 1);
29
+ var import_dotenv = __toESM(require("dotenv"), 1);
30
+ var import_citty = require("citty");
31
+
32
+ // package.json
33
+ var version = "1.0.0";
34
+
35
+ // src/main.ts
36
+ var import_consola = require("consola");
37
+ var log = (0, import_consola.createConsola)({
38
+ level: 3,
39
+ fancy: true
40
+ });
41
+ function getConfDir() {
42
+ let currentDir = process.cwd();
43
+ while (true) {
44
+ const configDir = import_path.default.join(currentDir, "config");
45
+ if (import_fs.default.existsSync(configDir) && import_fs.default.statSync(configDir).isDirectory()) {
46
+ return configDir;
47
+ }
48
+ const parentDir = import_path.default.dirname(currentDir);
49
+ if (parentDir === currentDir) {
50
+ throw new Error("Could not find config directory");
51
+ }
52
+ currentDir = parentDir;
53
+ }
54
+ }
55
+ function getEnvDir(env) {
56
+ const confDir = getConfDir();
57
+ const envDir = import_path.default.join(confDir, env);
58
+ if (import_fs.default.existsSync(envDir) && import_fs.default.statSync(envDir).isDirectory()) {
59
+ return envDir;
60
+ }
61
+ throw new Error(`Could not find directory ${envDir}`);
62
+ }
63
+ function validateEnvString(envVars) {
64
+ const lines = envVars.split("\n");
65
+ const validLineRegex = /^(\s*#.*|\s*$|[A-Za-z_][A-Za-z0-9_]*\s*=\s*.*)$/;
66
+ for (let i = 0; i < lines.length; i++) {
67
+ const line = lines[i].trim();
68
+ if (!validLineRegex.test(line)) {
69
+ throw new Error(`invalid .env format at line ${i + 1}: ${line}`);
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+ function getCompiledEnv(env) {
75
+ const envDir = getEnvDir(env);
76
+ const confDir = getConfDir();
77
+ const commonEnvPath = import_path.default.join(confDir, ".env");
78
+ const envEnvPath = import_path.default.join(envDir, ".env");
79
+ let envVars = "";
80
+ if (import_fs.default.existsSync(commonEnvPath)) {
81
+ envVars += import_fs.default.readFileSync(commonEnvPath, "utf-8");
82
+ }
83
+ if (import_fs.default.existsSync(envEnvPath)) {
84
+ envVars += "\n" + import_fs.default.readFileSync(envEnvPath, "utf-8");
85
+ }
86
+ validateEnvString(envVars);
87
+ return import_dotenv.default.parse(envVars);
88
+ }
89
+ function getEnvDetails(env) {
90
+ const compiledEnv = getCompiledEnv(env);
91
+ return Object.entries(compiledEnv).map(([key, value]) => `${key}=${value}`).join("\n");
92
+ }
93
+ function applyEnv(env) {
94
+ const compiledEnv = getCompiledEnv(env);
95
+ const envDir = getEnvDir(env);
96
+ process.env.ENV_DIR = envDir;
97
+ Object.entries(compiledEnv).forEach(([key, value]) => {
98
+ process.env[key] = value;
99
+ });
100
+ }
101
+ var main = (0, import_citty.defineCommand)({
102
+ meta: {
103
+ name: "envelope",
104
+ version,
105
+ description: "\u{1F4E8}"
106
+ },
107
+ subCommands: {
108
+ use: (0, import_citty.defineCommand)({
109
+ meta: {
110
+ name: "use",
111
+ description: "use an environment"
112
+ },
113
+ args: {
114
+ environment: {
115
+ type: "positional",
116
+ description: "the environment name"
117
+ }
118
+ },
119
+ async run({ args }) {
120
+ try {
121
+ applyEnv(args.environment);
122
+ log.success(`Using environment: ${args.environment}`);
123
+ } catch (error) {
124
+ log.error(error);
125
+ }
126
+ }
127
+ }),
128
+ get: (0, import_citty.defineCommand)({
129
+ meta: {
130
+ name: "get",
131
+ description: "prints environment details"
132
+ },
133
+ args: {
134
+ environment: {
135
+ type: "positional",
136
+ description: "the environment name",
137
+ required: false
138
+ }
139
+ },
140
+ async run({ args }) {
141
+ if (args.environment === void 0) {
142
+ const usage = await (0, import_citty.renderUsage)(this);
143
+ return log.info(usage);
144
+ }
145
+ try {
146
+ const details = getEnvDetails(args.environment);
147
+ log.info(details);
148
+ } catch (error) {
149
+ log.error(error);
150
+ }
151
+ }
152
+ }),
153
+ list: (0, import_citty.defineCommand)({
154
+ meta: {
155
+ name: "list",
156
+ description: "List all dev environment variables"
157
+ },
158
+ run() {
159
+ console.log("Listing all dev environment variables");
160
+ }
161
+ })
162
+ },
163
+ async run(opts) {
164
+ if (opts.args.help || opts.cmd === void 0) {
165
+ const usage = await (0, import_citty.renderUsage)(this);
166
+ log.info(usage);
167
+ return;
168
+ }
169
+ }
170
+ });
171
+
172
+ // src/index.ts
173
+ var import_citty2 = require("citty");
174
+ (0, import_citty2.runMain)(main);
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/main.ts
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import dotenv from "dotenv";
7
+ import { defineCommand, renderUsage } from "citty";
8
+
9
+ // package.json
10
+ var version = "1.0.0";
11
+
12
+ // src/main.ts
13
+ import { createConsola } from "consola";
14
+ var log = createConsola({
15
+ level: 3,
16
+ fancy: true
17
+ });
18
+ function getRootDir() {
19
+ let currentDir = process.cwd();
20
+ while (true) {
21
+ const configDir = path.join(currentDir, "env");
22
+ if (fs.existsSync(configDir) && fs.statSync(configDir).isDirectory()) {
23
+ return currentDir;
24
+ }
25
+ const parentDir = path.dirname(currentDir);
26
+ if (parentDir === currentDir) {
27
+ throw new Error("Could not find env directory");
28
+ }
29
+ currentDir = parentDir;
30
+ }
31
+ }
32
+ function getRootEnvDir() {
33
+ const rootDir = getRootDir();
34
+ return path.join(rootDir, "env");
35
+ }
36
+ function validateEnvString(envVars) {
37
+ const lines = envVars.split("\n");
38
+ const validLineRegex = /^(\s*#.*|\s*$|[A-Za-z_][A-Za-z0-9_]*\s*=\s*.*)$/;
39
+ for (let i = 0; i < lines.length; i++) {
40
+ const line = lines[i].trim();
41
+ if (!validLineRegex.test(line)) {
42
+ throw new Error(`invalid .env format at line ${i + 1}: ${line}`);
43
+ }
44
+ }
45
+ return true;
46
+ }
47
+ function assertSingleEnvMode(rootEnvDir) {
48
+ const entries = fs.readdirSync(rootEnvDir, { withFileTypes: true });
49
+ const hasSubdirs = entries.some(
50
+ (e) => e.isDirectory() && e.name !== "node_modules"
51
+ );
52
+ const hasFlatFiles = entries.some(
53
+ (e) => e.isFile() && /^\.env\../.test(e.name)
54
+ );
55
+ if (hasSubdirs && hasFlatFiles) {
56
+ throw new Error(
57
+ `Incompatible environment modes detected in '${rootEnvDir}': found both subdirectories (directory mode) and .env.* files (flat mode). Use one layout or the other, not both.`
58
+ );
59
+ }
60
+ }
61
+ async function getCompiledEnv(env, opts) {
62
+ const rootEnvDir = getRootEnvDir();
63
+ assertSingleEnvMode(rootEnvDir);
64
+ const envSubDir = path.join(rootEnvDir, env);
65
+ const flatEnvFile = path.join(rootEnvDir, `.env.${env}`);
66
+ const isDirectoryMode = fs.existsSync(envSubDir) && fs.statSync(envSubDir).isDirectory();
67
+ const isFlatMode = !isDirectoryMode && fs.existsSync(flatEnvFile);
68
+ if (!isDirectoryMode && !isFlatMode) {
69
+ throw new Error(
70
+ `Could not find environment '${env}': no directory '${envSubDir}' or file '${flatEnvFile}'`
71
+ );
72
+ }
73
+ const envDir = isDirectoryMode ? envSubDir : rootEnvDir;
74
+ let envVars = "";
75
+ envVars += `ENVELOPE_ENV=${env}
76
+ `;
77
+ envVars += `ENVELOPE_DIR=${envDir}
78
+ `;
79
+ const envFiles = [
80
+ { path: path.join(rootEnvDir, ".env"), name: "common" },
81
+ {
82
+ path: isDirectoryMode ? path.join(envSubDir, ".env") : flatEnvFile,
83
+ name: env
84
+ }
85
+ ];
86
+ for (const file of envFiles) {
87
+ if (fs.existsSync(file.path)) {
88
+ if (opts?.silent !== true)
89
+ log.info(`Reading ${file.name} environment variables from ${file.path}`);
90
+ envVars += "\n" + fs.readFileSync(file.path, "utf-8");
91
+ }
92
+ }
93
+ validateEnvString(envVars);
94
+ return dotenv.parse(envVars);
95
+ }
96
+ async function compileDotEnv(env, silent) {
97
+ const compiledEnv = await getCompiledEnv(env, {
98
+ silent
99
+ });
100
+ return Object.entries(compiledEnv).map(([key, value]) => `${key}=${value}`).join("\n");
101
+ }
102
+ var main = defineCommand({
103
+ meta: {
104
+ name: "envelope",
105
+ version,
106
+ description: "\u{1F4E8}"
107
+ },
108
+ subCommands: {
109
+ get: defineCommand({
110
+ meta: {
111
+ name: "get",
112
+ description: "Print environment variables to console"
113
+ },
114
+ args: {
115
+ silent: {
116
+ type: "boolean",
117
+ description: "Do not log status messages",
118
+ alias: ["s"]
119
+ },
120
+ environment: {
121
+ type: "positional",
122
+ description: "The environment name",
123
+ required: true
124
+ }
125
+ },
126
+ async run({ args }) {
127
+ try {
128
+ const silent = !!args.silent;
129
+ const env = await compileDotEnv(args.environment, silent);
130
+ log.box(env);
131
+ } catch (error) {
132
+ log.error(`Error in 'get' command: ${error.message}`);
133
+ }
134
+ }
135
+ }),
136
+ use: defineCommand({
137
+ meta: {
138
+ name: "use",
139
+ description: "Compile a .env file for the given environment"
140
+ },
141
+ args: {
142
+ silent: {
143
+ type: "boolean",
144
+ description: "Do not log status messages",
145
+ alias: ["s"]
146
+ },
147
+ environment: {
148
+ type: "positional",
149
+ description: "The environment name",
150
+ required: true
151
+ }
152
+ },
153
+ async run({ args }) {
154
+ const silent = !!args.silent;
155
+ try {
156
+ if (!silent) {
157
+ log.info(`Compiling environment variables for ${args.environment}`);
158
+ }
159
+ const env = await compileDotEnv(args.environment, silent);
160
+ const rootDir = getRootDir();
161
+ if (!silent) {
162
+ log.success(
163
+ `Compiled environment variables for ${args.environment}: to ${rootDir}/.env`
164
+ // TODO: this is not correct
165
+ );
166
+ }
167
+ fs.writeFileSync(path.join(rootDir, ".env"), env, "utf-8");
168
+ } catch (error) {
169
+ if (!silent) log.error(error);
170
+ process.exit(1);
171
+ }
172
+ }
173
+ }),
174
+ list: defineCommand({
175
+ meta: {
176
+ name: "list",
177
+ description: "List all available environments"
178
+ },
179
+ run() {
180
+ try {
181
+ const rootEnvDir = getRootEnvDir();
182
+ assertSingleEnvMode(rootEnvDir);
183
+ const entries = fs.readdirSync(rootEnvDir, { withFileTypes: true });
184
+ const environments = [
185
+ ...entries.filter((e) => e.isDirectory() && e.name !== "node_modules").map((e) => e.name),
186
+ ...entries.filter((e) => e.isFile() && /^\.env\../.test(e.name)).map((e) => e.name.slice(5))
187
+ ];
188
+ log.info("Available environments: " + environments.join(", "));
189
+ } catch (error) {
190
+ log.error(`Error listing environments: ${error.message}`);
191
+ process.exit(1);
192
+ }
193
+ }
194
+ }),
195
+ current: defineCommand({
196
+ meta: {
197
+ name: "current",
198
+ description: "Print current environment"
199
+ },
200
+ run() {
201
+ try {
202
+ const rootDir = getRootDir();
203
+ const envFilePath = path.join(rootDir, ".env");
204
+ if (!fs.existsSync(envFilePath)) {
205
+ throw new Error("Oopsie");
206
+ }
207
+ const envString = fs.readFileSync(envFilePath, "utf-8");
208
+ validateEnvString(envString);
209
+ const parsed = dotenv.parse(envString);
210
+ if (!parsed.ENVELOPE_ENV) {
211
+ throw new Error("Could not parse ENVELOPE_ENV");
212
+ }
213
+ log.info(`The current environment is: ${parsed.ENVELOPE_ENV}`);
214
+ } catch (error) {
215
+ log.error(`Error getting current environment: ${error.message}`);
216
+ process.exit(1);
217
+ }
218
+ }
219
+ })
220
+ },
221
+ args: {
222
+ help: {
223
+ type: "boolean",
224
+ description: "show help"
225
+ }
226
+ },
227
+ async run(opts) {
228
+ if (opts.args.help || opts.rawArgs.length === 0) {
229
+ const usage = await renderUsage(this);
230
+ log.info(usage);
231
+ return;
232
+ }
233
+ }
234
+ });
235
+
236
+ // src/index.ts
237
+ import { runMain } from "citty";
238
+ await runMain(main);
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "envelope-env",
3
+ "version": "1.0.0",
4
+ "main": "./dist/index.js",
5
+ "scripts": {
6
+ "build": "tsup src/index.ts --format esm --dts --target es2022",
7
+ "start": "node dist/index.js",
8
+ "dev": "tsup src/index.ts --format esm,cjs --watch --onSuccess \"node dist/index.js\"",
9
+ "test": "vitest"
10
+ },
11
+ "keywords": ["env", "environment variables", "dx"],
12
+ "author": "Maximillian George",
13
+ "license": "ISC",
14
+ "description": "Organise multiple environments in /env and compile into root .env by name",
15
+ "dependencies": {
16
+ "c12": "^1.11.2",
17
+ "citty": "^0.1.6",
18
+ "consola": "^3.2.3",
19
+ "defu": "^6.1.4",
20
+ "dotenv": "^16.4.5",
21
+ "glob": "^11.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/dotenv": "^6.1.1",
25
+ "@types/node": "^22.5.5",
26
+ "memfs": "^4.12.0",
27
+ "tsup": "^8.3.0",
28
+ "typescript": "^5.6.2",
29
+ "vite": "^5.4.7",
30
+ "vitest": "^2.1.1"
31
+ },
32
+ "bin": {
33
+ "envelope": "./dist/index.js"
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "type": "module"
39
+ }