morphix-env 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/README.md +272 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +376 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# mx-env
|
|
2
|
+
|
|
3
|
+
Environment variable toolkit for multi-project architectures. Combines [Infisical](https://infisical.com) secret management with local override files and client-side runtime injection.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
| Problem | Solution |
|
|
8
|
+
|---------|----------|
|
|
9
|
+
| `NEXT_PUBLIC_*` / `VITE_*` baked at build time | `mx-env generate` creates `__env.js` for runtime injection |
|
|
10
|
+
| Scattered env vars across hosting platforms | Single source of truth in Infisical, pulled at startup |
|
|
11
|
+
| No local override when using remote config | `.env.local` always wins — edit one file, restart |
|
|
12
|
+
| Different tools for different needs (dotenv, cross-env, infisical CLI) | One tool, one command |
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add -D mx-env
|
|
18
|
+
# or
|
|
19
|
+
npm install -D mx-env
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Run a command with .env.local overrides
|
|
26
|
+
mx-env run -- next dev
|
|
27
|
+
|
|
28
|
+
# Generate client-side __env.js
|
|
29
|
+
mx-env generate --out public/__env.js
|
|
30
|
+
|
|
31
|
+
# Debug: see what's loaded
|
|
32
|
+
mx-env inspect
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
### `mx-env run [options] -- <command>`
|
|
38
|
+
|
|
39
|
+
Load environment variables, then execute a command. The child process inherits all injected vars.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Basic: load .env.local, run dev server
|
|
43
|
+
mx-env run -- next dev --turbo -p 3004
|
|
44
|
+
|
|
45
|
+
# Custom env file
|
|
46
|
+
mx-env run -f .env.staging -- npm start
|
|
47
|
+
|
|
48
|
+
# Skip Infisical (use only local files)
|
|
49
|
+
mx-env run --no-infisical -- npm start
|
|
50
|
+
|
|
51
|
+
# Verbose: show which vars were loaded
|
|
52
|
+
mx-env run -v -- node server.js
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Loading priority (high to low):**
|
|
56
|
+
|
|
57
|
+
1. `.env.local` (or `--env-file`) — always wins
|
|
58
|
+
2. Infisical secrets — pulled via SDK
|
|
59
|
+
3. Existing `process.env` — Docker ENV, CI vars, etc.
|
|
60
|
+
|
|
61
|
+
### `mx-env generate [options]`
|
|
62
|
+
|
|
63
|
+
Extract public environment variables (`NEXT_PUBLIC_*`, `VITE_*`, `EXPO_PUBLIC_*`) and write them to a JS file for browser runtime injection.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Default: public/__env.js
|
|
67
|
+
mx-env generate
|
|
68
|
+
|
|
69
|
+
# Custom output path (Vite projects)
|
|
70
|
+
mx-env generate --out dist/__env.js
|
|
71
|
+
|
|
72
|
+
# Only include specific prefix
|
|
73
|
+
mx-env generate --filter NEXT_PUBLIC_
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Output file content:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
window.__ENV={"NEXT_PUBLIC_API_URL":"https://api.example.com","NEXT_PUBLIC_APP_NAME":"MyApp"};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Load it in your HTML before your app bundle:
|
|
83
|
+
|
|
84
|
+
```html
|
|
85
|
+
<script src="/__env.js"></script>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Read in your app:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
function getEnv(key: string, fallback = ''): string {
|
|
92
|
+
if (typeof window === 'undefined') return process.env[key] || fallback
|
|
93
|
+
return window.__ENV?.[key] || process.env[key] || fallback
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `mx-env inspect [options]`
|
|
98
|
+
|
|
99
|
+
Print env var values for debugging. Secrets are masked (first 4 chars shown).
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
mx-env inspect
|
|
103
|
+
mx-env inspect --filter NEXT_PUBLIC_
|
|
104
|
+
mx-env inspect -f .env.production
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Options
|
|
108
|
+
|
|
109
|
+
| Flag | Short | Description |
|
|
110
|
+
|------|-------|-------------|
|
|
111
|
+
| `--env-file <path>` | `-f` | Env file to load (default: `.env.local`, repeatable) |
|
|
112
|
+
| `--out <path>` | `-o` | Output path for generate (default: `public/__env.js`) |
|
|
113
|
+
| `--filter <prefix>` | | Only include vars with this prefix |
|
|
114
|
+
| `--no-infisical` | | Skip Infisical SDK fetch |
|
|
115
|
+
| `--verbose` | `-v` | Show loaded variable names |
|
|
116
|
+
|
|
117
|
+
## Config File
|
|
118
|
+
|
|
119
|
+
Create `mx-env.config.json` in your project root to declare project-level settings. This file is committed to git.
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"infisical": {
|
|
124
|
+
"projectId": "your-project-id",
|
|
125
|
+
"paths": ["/ai/shared", "/ai/web"],
|
|
126
|
+
"env": "$DEPLOY_ENV"
|
|
127
|
+
},
|
|
128
|
+
"envFiles": [".env.local"],
|
|
129
|
+
"generate": {
|
|
130
|
+
"out": "public/__env.js",
|
|
131
|
+
"filter": "NEXT_PUBLIC_"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Field | Description | Committed to git? |
|
|
137
|
+
|-------|-------------|-------------------|
|
|
138
|
+
| `infisical.projectId` | Infisical project ID | Yes — not a secret |
|
|
139
|
+
| `infisical.paths` | Secret paths to pull | Yes — not a secret |
|
|
140
|
+
| `infisical.env` | Environment name, supports `$VAR` references | Yes |
|
|
141
|
+
| `envFiles` | Local override files to load | Yes |
|
|
142
|
+
| `generate` | Client-side __env.js output config | Yes |
|
|
143
|
+
|
|
144
|
+
**Infisical authentication** is always via environment variables — never in the config file:
|
|
145
|
+
|
|
146
|
+
| Env Var | Description |
|
|
147
|
+
|---------|-------------|
|
|
148
|
+
| `INFISICAL_CLIENT_ID` | Machine Identity client ID |
|
|
149
|
+
| `INFISICAL_CLIENT_SECRET` | Machine Identity client secret |
|
|
150
|
+
| `DEPLOY_ENV` | Environment name (`dev` / `staging` / `prod`) |
|
|
151
|
+
|
|
152
|
+
## Usage Examples
|
|
153
|
+
|
|
154
|
+
### Next.js
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"scripts": {
|
|
159
|
+
"dev": "mx-env run -- next dev --turbo -p 3004",
|
|
160
|
+
"build": "mx-env run -- next build",
|
|
161
|
+
"start": "mx-env run -- node server.js"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`mx-env.config.json`:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"infisical": {
|
|
171
|
+
"projectId": "xxx",
|
|
172
|
+
"paths": ["/ai/shared", "/ai/web"],
|
|
173
|
+
"env": "$DEPLOY_ENV"
|
|
174
|
+
},
|
|
175
|
+
"envFiles": [".env.local"],
|
|
176
|
+
"generate": {
|
|
177
|
+
"out": "public/__env.js",
|
|
178
|
+
"filter": "NEXT_PUBLIC_"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Vite (React / Vue / Ionic)
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"scripts": {
|
|
188
|
+
"dev": "mx-env run -- vite",
|
|
189
|
+
"build": "mx-env run -- vite build"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"infisical": {
|
|
197
|
+
"projectId": "xxx",
|
|
198
|
+
"paths": ["/ai/shared", "/ai/shell"],
|
|
199
|
+
"env": "$DEPLOY_ENV"
|
|
200
|
+
},
|
|
201
|
+
"generate": {
|
|
202
|
+
"out": "dist/__env.js",
|
|
203
|
+
"filter": "VITE_"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Express API
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"scripts": {
|
|
213
|
+
"dev": "mx-env run -- tsx watch src/index.ts",
|
|
214
|
+
"start": "mx-env run -- node dist/index.js"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"infisical": {
|
|
222
|
+
"projectId": "xxx",
|
|
223
|
+
"paths": ["/ai/shared", "/ai/api"],
|
|
224
|
+
"env": "$DEPLOY_ENV"
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
No `generate` field — server-side apps don't need `__env.js`.
|
|
230
|
+
|
|
231
|
+
### Docker
|
|
232
|
+
|
|
233
|
+
```dockerfile
|
|
234
|
+
FROM node:20-alpine
|
|
235
|
+
WORKDIR /app
|
|
236
|
+
COPY . .
|
|
237
|
+
RUN pnpm install && pnpm build
|
|
238
|
+
|
|
239
|
+
# Only these 3 vars needed at runtime
|
|
240
|
+
ENV INFISICAL_CLIENT_ID=""
|
|
241
|
+
ENV INFISICAL_CLIENT_SECRET=""
|
|
242
|
+
ENV DEPLOY_ENV="prod"
|
|
243
|
+
|
|
244
|
+
CMD ["npx", "mx-env", "run", "--", "node", "server.js"]
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
No Infisical CLI binary needed in the image.
|
|
248
|
+
|
|
249
|
+
### Local Development with Infisical CLI
|
|
250
|
+
|
|
251
|
+
If you already use `infisical run` locally, mx-env still adds value as the override layer:
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{
|
|
255
|
+
"dev": "infisical run --path=/ai --env=dev -- mx-env run -- next dev",
|
|
256
|
+
"dev:local": "infisical run --path=/ai --env=dev -- mx-env run -- next dev"
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`.env.local` overrides take effect on top of Infisical CLI injection.
|
|
261
|
+
|
|
262
|
+
## How It Works
|
|
263
|
+
|
|
264
|
+
1. Read `mx-env.config.json` for project settings
|
|
265
|
+
2. If `INFISICAL_CLIENT_ID` + `INFISICAL_CLIENT_SECRET` exist → fetch secrets via SDK, inject into `process.env` (does not overwrite existing vars)
|
|
266
|
+
3. Read `.env.local` → inject into `process.env` (overwrites everything, highest priority)
|
|
267
|
+
4. If `generate` is configured → write `__env.js` with public vars
|
|
268
|
+
5. Spawn child command — it inherits the fully assembled `process.env`
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
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/env.ts
|
|
27
|
+
var import_fs = require("fs");
|
|
28
|
+
var import_path = require("path");
|
|
29
|
+
function parseEnvFile(filePath) {
|
|
30
|
+
const absPath = (0, import_path.resolve)(filePath);
|
|
31
|
+
if (!(0, import_fs.existsSync)(absPath)) return {};
|
|
32
|
+
const vars = {};
|
|
33
|
+
const content = (0, import_fs.readFileSync)(absPath, "utf8");
|
|
34
|
+
for (const raw of content.split("\n")) {
|
|
35
|
+
const line = raw.trim();
|
|
36
|
+
if (!line || line.startsWith("#")) continue;
|
|
37
|
+
const eq = line.indexOf("=");
|
|
38
|
+
if (eq === -1) continue;
|
|
39
|
+
const key = line.slice(0, eq).trim();
|
|
40
|
+
let value = line.slice(eq + 1).trim();
|
|
41
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
42
|
+
value = value.slice(1, -1);
|
|
43
|
+
}
|
|
44
|
+
vars[key] = value;
|
|
45
|
+
}
|
|
46
|
+
return vars;
|
|
47
|
+
}
|
|
48
|
+
function loadEnvFiles(files) {
|
|
49
|
+
const overrides = [];
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
const vars = parseEnvFile(file);
|
|
52
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
53
|
+
process.env[key] = value;
|
|
54
|
+
overrides.push({ key, source: file });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return overrides;
|
|
58
|
+
}
|
|
59
|
+
var PUBLIC_PREFIXES = ["NEXT_PUBLIC_", "VITE_", "EXPO_PUBLIC_"];
|
|
60
|
+
function extractPublicVars() {
|
|
61
|
+
const vars = {};
|
|
62
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
63
|
+
if (value && PUBLIC_PREFIXES.some((p) => key.startsWith(p))) {
|
|
64
|
+
vars[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return vars;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/infisical.ts
|
|
71
|
+
var import_sdk = require("@infisical/sdk");
|
|
72
|
+
function getInfisicalConfig() {
|
|
73
|
+
const clientId = process.env.INFISICAL_CLIENT_ID;
|
|
74
|
+
const clientSecret = process.env.INFISICAL_CLIENT_SECRET;
|
|
75
|
+
const projectId = process.env.INFISICAL_PROJECT_ID;
|
|
76
|
+
if (!clientId || !clientSecret || !projectId) return null;
|
|
77
|
+
return {
|
|
78
|
+
clientId,
|
|
79
|
+
clientSecret,
|
|
80
|
+
projectId,
|
|
81
|
+
environment: process.env.DEPLOY_ENV || process.env.INFISICAL_ENV || "dev",
|
|
82
|
+
paths: (process.env.INFISICAL_PATHS || "/").split(",").map((p) => p.trim()),
|
|
83
|
+
siteUrl: process.env.INFISICAL_SITE_URL
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function fetchInfisicalSecrets(config) {
|
|
87
|
+
const client = new import_sdk.InfisicalSDK({
|
|
88
|
+
...config.siteUrl ? { siteUrl: config.siteUrl } : {}
|
|
89
|
+
});
|
|
90
|
+
await client.auth().universalAuth.login({
|
|
91
|
+
clientId: config.clientId,
|
|
92
|
+
clientSecret: config.clientSecret
|
|
93
|
+
});
|
|
94
|
+
let count = 0;
|
|
95
|
+
for (const secretPath of config.paths) {
|
|
96
|
+
const result = await client.secrets().listSecrets({
|
|
97
|
+
environment: config.environment,
|
|
98
|
+
projectId: config.projectId,
|
|
99
|
+
secretPath,
|
|
100
|
+
expandSecretReferences: true,
|
|
101
|
+
viewSecretValue: true
|
|
102
|
+
});
|
|
103
|
+
for (const secret of result.secrets) {
|
|
104
|
+
process.env[secret.secretKey] = secret.secretValue;
|
|
105
|
+
count++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return count;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/config.ts
|
|
112
|
+
var import_fs2 = require("fs");
|
|
113
|
+
var import_path2 = require("path");
|
|
114
|
+
var CONFIG_FILE = "mx-env.config.json";
|
|
115
|
+
function resolveEnvRef(value) {
|
|
116
|
+
if (value.startsWith("$")) {
|
|
117
|
+
return process.env[value.slice(1)] || "";
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
function loadConfig() {
|
|
122
|
+
const configPath = (0, import_path2.resolve)(CONFIG_FILE);
|
|
123
|
+
if (!(0, import_fs2.existsSync)(configPath)) return {};
|
|
124
|
+
try {
|
|
125
|
+
const raw = JSON.parse((0, import_fs2.readFileSync)(configPath, "utf8"));
|
|
126
|
+
if (raw.infisical?.env) {
|
|
127
|
+
raw.infisical.env = resolveEnvRef(raw.infisical.env);
|
|
128
|
+
}
|
|
129
|
+
return raw;
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.warn(`[mx-env] Failed to parse ${CONFIG_FILE}: ${e.message}`);
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/cli.ts
|
|
137
|
+
var import_cross_spawn = __toESM(require("cross-spawn"));
|
|
138
|
+
var import_fs3 = require("fs");
|
|
139
|
+
var import_path3 = require("path");
|
|
140
|
+
var VERSION = "0.1.0";
|
|
141
|
+
var DEFAULT_ENV_FILE = ".env.local";
|
|
142
|
+
function parseArgs(argv) {
|
|
143
|
+
const args = {
|
|
144
|
+
command: "",
|
|
145
|
+
subArgs: [],
|
|
146
|
+
envFiles: [],
|
|
147
|
+
outFile: null,
|
|
148
|
+
verbose: false,
|
|
149
|
+
filter: null,
|
|
150
|
+
noInfisical: false
|
|
151
|
+
};
|
|
152
|
+
let i = 2;
|
|
153
|
+
const command = argv[i];
|
|
154
|
+
if (!command) return args;
|
|
155
|
+
args.command = command;
|
|
156
|
+
i++;
|
|
157
|
+
while (i < argv.length) {
|
|
158
|
+
const arg = argv[i];
|
|
159
|
+
if (arg === "--") {
|
|
160
|
+
args.subArgs = argv.slice(i + 1);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
if (arg === "--env-file" || arg === "-f") {
|
|
164
|
+
i++;
|
|
165
|
+
if (argv[i]) args.envFiles.push(argv[i]);
|
|
166
|
+
} else if (arg === "--out" || arg === "-o") {
|
|
167
|
+
i++;
|
|
168
|
+
if (argv[i]) args.outFile = argv[i];
|
|
169
|
+
} else if (arg === "--filter") {
|
|
170
|
+
i++;
|
|
171
|
+
if (argv[i]) args.filter = argv[i];
|
|
172
|
+
} else if (arg === "--verbose" || arg === "-v") {
|
|
173
|
+
args.verbose = true;
|
|
174
|
+
} else if (arg === "--no-infisical") {
|
|
175
|
+
args.noInfisical = true;
|
|
176
|
+
} else {
|
|
177
|
+
if (args.command === "run") {
|
|
178
|
+
args.subArgs = argv.slice(i);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
i++;
|
|
183
|
+
}
|
|
184
|
+
return args;
|
|
185
|
+
}
|
|
186
|
+
function mergeWithConfig(args, config) {
|
|
187
|
+
if (args.envFiles.length === 0) {
|
|
188
|
+
args.envFiles = config.envFiles || [DEFAULT_ENV_FILE];
|
|
189
|
+
}
|
|
190
|
+
if (!args.outFile && config.generate) {
|
|
191
|
+
args.outFile = config.generate.out;
|
|
192
|
+
}
|
|
193
|
+
if (!args.filter && config.generate?.filter) {
|
|
194
|
+
args.filter = config.generate.filter;
|
|
195
|
+
}
|
|
196
|
+
return args;
|
|
197
|
+
}
|
|
198
|
+
async function loadAllEnv(args, config) {
|
|
199
|
+
if (!args.noInfisical) {
|
|
200
|
+
const infisicalConfig = config.infisical ? {
|
|
201
|
+
clientId: process.env.INFISICAL_CLIENT_ID || "",
|
|
202
|
+
clientSecret: process.env.INFISICAL_CLIENT_SECRET || "",
|
|
203
|
+
projectId: config.infisical.projectId,
|
|
204
|
+
environment: config.infisical.env || "dev",
|
|
205
|
+
paths: config.infisical.paths || ["/"],
|
|
206
|
+
siteUrl: config.infisical.siteUrl
|
|
207
|
+
} : getInfisicalConfig();
|
|
208
|
+
if (infisicalConfig && infisicalConfig.clientId && infisicalConfig.clientSecret) {
|
|
209
|
+
try {
|
|
210
|
+
const count = await fetchInfisicalSecrets(infisicalConfig);
|
|
211
|
+
console.log(`[mx-env] Infisical: loaded ${count} secrets (${infisicalConfig.environment}: ${infisicalConfig.paths.join(", ")})`);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
console.warn(`[mx-env] Infisical: failed - ${e.message}`);
|
|
214
|
+
}
|
|
215
|
+
} else if (args.verbose) {
|
|
216
|
+
console.log("[mx-env] Infisical: skipped (no credentials)");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const overrides = loadEnvFiles(args.envFiles);
|
|
220
|
+
if (overrides.length > 0) {
|
|
221
|
+
console.log(`[mx-env] Loaded ${overrides.length} overrides from ${args.envFiles.join(", ")}`);
|
|
222
|
+
if (args.verbose) {
|
|
223
|
+
for (const o of overrides) {
|
|
224
|
+
console.log(` ${o.key} (from ${o.source})`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function cmdRun(args, config) {
|
|
230
|
+
if (args.subArgs.length === 0) {
|
|
231
|
+
console.error("[mx-env] No command specified. Usage: mx-env run -- <command>");
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
await loadAllEnv(args, config);
|
|
235
|
+
if (args.outFile) {
|
|
236
|
+
generateClientEnv(args.outFile, args.filter);
|
|
237
|
+
}
|
|
238
|
+
const [cmd, ...cmdArgs] = args.subArgs;
|
|
239
|
+
const result = import_cross_spawn.default.sync(cmd, cmdArgs, {
|
|
240
|
+
stdio: "inherit",
|
|
241
|
+
env: process.env
|
|
242
|
+
});
|
|
243
|
+
if (result.error) {
|
|
244
|
+
console.error(`[mx-env] Failed to execute: ${cmd}`, result.error.message);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
process.exit(result.status ?? 1);
|
|
248
|
+
}
|
|
249
|
+
function generateClientEnv(outFile, filter) {
|
|
250
|
+
let vars = extractPublicVars();
|
|
251
|
+
if (filter) {
|
|
252
|
+
vars = Object.fromEntries(
|
|
253
|
+
Object.entries(vars).filter(([key]) => key.startsWith(filter))
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const js = `window.__ENV=${JSON.stringify(vars)};`;
|
|
257
|
+
(0, import_fs3.mkdirSync)((0, import_path3.dirname)(outFile), { recursive: true });
|
|
258
|
+
(0, import_fs3.writeFileSync)(outFile, js);
|
|
259
|
+
console.log(`[mx-env] Generated ${outFile} (${Object.keys(vars).length} client vars)`);
|
|
260
|
+
}
|
|
261
|
+
async function cmdGenerate(args, config) {
|
|
262
|
+
const outFile = args.outFile || "public/__env.js";
|
|
263
|
+
await loadAllEnv(args, config);
|
|
264
|
+
generateClientEnv(outFile, args.filter);
|
|
265
|
+
if (args.verbose) {
|
|
266
|
+
const vars = extractPublicVars();
|
|
267
|
+
for (const key of Object.keys(vars)) {
|
|
268
|
+
console.log(` ${key}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function cmdInspect(args) {
|
|
273
|
+
for (const file of args.envFiles.length > 0 ? args.envFiles : [DEFAULT_ENV_FILE]) {
|
|
274
|
+
const vars = parseEnvFile(file);
|
|
275
|
+
const keys = Object.keys(vars);
|
|
276
|
+
if (keys.length === 0) {
|
|
277
|
+
console.log(`${file}: (not found or empty)`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
console.log(`${file}: (${keys.length} vars)`);
|
|
281
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
282
|
+
if (args.filter && !key.startsWith(args.filter)) continue;
|
|
283
|
+
const display = value.length > 8 ? value.slice(0, 4) + "***" : value;
|
|
284
|
+
console.log(` ${key}=${display}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const publicVars = extractPublicVars();
|
|
288
|
+
const filtered = args.filter ? Object.entries(publicVars).filter(([k]) => k.startsWith(args.filter)) : Object.entries(publicVars);
|
|
289
|
+
if (filtered.length > 0) {
|
|
290
|
+
console.log(`
|
|
291
|
+
process.env public vars: (${filtered.length})`);
|
|
292
|
+
for (const [key, value] of filtered) {
|
|
293
|
+
const display = value.length > 8 ? value.slice(0, 4) + "***" : value;
|
|
294
|
+
console.log(` ${key}=${display}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function showHelp() {
|
|
299
|
+
console.log(`
|
|
300
|
+
mx-env v${VERSION} \u2014 MorphixAI environment variable toolkit
|
|
301
|
+
|
|
302
|
+
Usage:
|
|
303
|
+
mx-env run [options] -- <command> Load env + exec command
|
|
304
|
+
mx-env generate [options] Generate __env.js for client-side runtime
|
|
305
|
+
mx-env inspect [options] Print env vars for debugging
|
|
306
|
+
|
|
307
|
+
Options:
|
|
308
|
+
-f, --env-file <path> Env file to load (default: .env.local, repeatable)
|
|
309
|
+
-o, --out <path> Output path for generate (default: public/__env.js)
|
|
310
|
+
--filter <prefix> Only include vars with this prefix
|
|
311
|
+
--no-infisical Skip Infisical SDK fetch
|
|
312
|
+
-v, --verbose Show loaded variable names
|
|
313
|
+
--help, -h Show this help
|
|
314
|
+
--version Show version
|
|
315
|
+
|
|
316
|
+
Config file (mx-env.config.json):
|
|
317
|
+
{
|
|
318
|
+
"infisical": {
|
|
319
|
+
"projectId": "xxx",
|
|
320
|
+
"paths": ["/ai/shared", "/ai/web"],
|
|
321
|
+
"env": "$DEPLOY_ENV"
|
|
322
|
+
},
|
|
323
|
+
"envFiles": [".env.local"],
|
|
324
|
+
"generate": {
|
|
325
|
+
"out": "public/__env.js",
|
|
326
|
+
"filter": "NEXT_PUBLIC_"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
Env loading priority (high \u2192 low):
|
|
331
|
+
1. .env.local (or --env-file)
|
|
332
|
+
2. Infisical secrets
|
|
333
|
+
3. Existing process.env
|
|
334
|
+
|
|
335
|
+
Examples:
|
|
336
|
+
mx-env run -- next dev --turbo -p 3004
|
|
337
|
+
mx-env run --no-infisical -- npm start
|
|
338
|
+
mx-env run -f .env.staging -- npm start
|
|
339
|
+
mx-env generate --out dist/__env.js
|
|
340
|
+
mx-env inspect --filter VITE_
|
|
341
|
+
`);
|
|
342
|
+
}
|
|
343
|
+
async function main() {
|
|
344
|
+
const config = loadConfig();
|
|
345
|
+
const args = mergeWithConfig(parseArgs(process.argv), config);
|
|
346
|
+
switch (args.command) {
|
|
347
|
+
case "run":
|
|
348
|
+
await cmdRun(args, config);
|
|
349
|
+
break;
|
|
350
|
+
case "generate":
|
|
351
|
+
case "gen":
|
|
352
|
+
await cmdGenerate(args, config);
|
|
353
|
+
break;
|
|
354
|
+
case "inspect":
|
|
355
|
+
cmdInspect(args);
|
|
356
|
+
break;
|
|
357
|
+
case "--help":
|
|
358
|
+
case "-h":
|
|
359
|
+
case "help":
|
|
360
|
+
showHelp();
|
|
361
|
+
break;
|
|
362
|
+
case "--version":
|
|
363
|
+
console.log(VERSION);
|
|
364
|
+
break;
|
|
365
|
+
default:
|
|
366
|
+
if (args.command) {
|
|
367
|
+
console.error(`[mx-env] Unknown command: ${args.command}`);
|
|
368
|
+
}
|
|
369
|
+
showHelp();
|
|
370
|
+
process.exit(args.command ? 1 : 0);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
main().catch((e) => {
|
|
374
|
+
console.error("[mx-env] Fatal:", e.message);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "morphix-env",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Environment variable toolkit — Infisical integration, local overrides, and client-side runtime injection for Next.js / Vite / Express.",
|
|
5
|
+
"author": "MorphixAI <dev@morphixai.com>",
|
|
6
|
+
"homepage": "https://github.com/Morphicai/mx-env#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Morphicai/mx-env.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Morphicai/mx-env/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"morphix-env": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup src/cli.ts --format cjs --dts --clean",
|
|
19
|
+
"dev": "tsup src/cli.ts --format cjs --watch",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"prepublishOnly": "pnpm build && pnpm test"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"env",
|
|
29
|
+
"dotenv",
|
|
30
|
+
"environment-variables",
|
|
31
|
+
"cross-env",
|
|
32
|
+
"infisical",
|
|
33
|
+
"secrets",
|
|
34
|
+
"runtime-env",
|
|
35
|
+
"nextjs",
|
|
36
|
+
"vite",
|
|
37
|
+
"docker",
|
|
38
|
+
"cli"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/cross-spawn": "^6.0.6",
|
|
46
|
+
"tsup": "^8.0.0",
|
|
47
|
+
"typescript": "^5.0.0",
|
|
48
|
+
"vitest": "^4.1.1"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@infisical/sdk": "^5.0.0",
|
|
52
|
+
"cross-spawn": "^7.0.6"
|
|
53
|
+
}
|
|
54
|
+
}
|