@tltdh61/dotenvrtdb 1.260131.11434
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 +526 -0
- package/cli.js +736 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) 2025 Jens Claes
|
|
2
|
+
|
|
3
|
+
The MIT License (MIT)
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# dotenvrtdb
|
|
2
|
+
|
|
3
|
+
A simple dotenv CLI for loading environment variables from `.env` files **with remote realtime database support**.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@tolaptrinhdh61-spec/dotenvrtdb)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
✨ All standard dotenv-cli features
|
|
11
|
+
🔥 **Pull** environment variables from remote databases (Firebase, custom APIs)
|
|
12
|
+
🚀 **Push** local .env files to remote databases
|
|
13
|
+
🔒 Automatic auth token masking in console output
|
|
14
|
+
📦 Support for multiple file formats and cascading environments
|
|
15
|
+
🌐 Works with HTTP/HTTPS endpoints
|
|
16
|
+
|
|
17
|
+
## Installing
|
|
18
|
+
|
|
19
|
+
### NPM
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
$ npm install -g @tolaptrinhdh61-spec/dotenvrtdb
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Yarn
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
$ yarn global add @tolaptrinhdh61-spec/dotenvrtdb
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### pnpm
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
$ pnpm add -g @tolaptrinhdh61-spec/dotenvrtdb
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### GitHub Packages
|
|
38
|
+
|
|
39
|
+
To install from GitHub Packages, create a `.npmrc` file:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
@tolaptrinhdh61-spec:registry=https://npm.pkg.github.com
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then install:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
$ npm install -g @tolaptrinhdh61-spec/dotenvrtdb
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Basic Usage
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
$ dotenvrtdb -- <command with arguments>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This will load the variables from the .env file in the current working directory and then run the command (using the new set of environment variables).
|
|
60
|
+
|
|
61
|
+
Alternatively, if you do not need to pass arguments to the command, you can use the shorthand:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
$ dotenvrtdb <command>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 🔥 Remote Database Sync
|
|
68
|
+
|
|
69
|
+
#### Pull environment variables from remote database
|
|
70
|
+
|
|
71
|
+
Download environment variables from a realtime database (Firebase, custom API, etc.) and save to a local `.env` file:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Pull to default .env file
|
|
75
|
+
$ dotenvrtdb --pull https://your-project.firebaseio.com/env.json
|
|
76
|
+
|
|
77
|
+
# Pull to custom file using -e flag
|
|
78
|
+
$ dotenvrtdb --pull https://your-project.firebaseio.com/env.json -e .env.production
|
|
79
|
+
|
|
80
|
+
# Or specify -e flag before --pull
|
|
81
|
+
$ dotenvrtdb -e .env.staging --pull https://your-project.firebaseio.com/env.json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### Push environment variables to remote database
|
|
85
|
+
|
|
86
|
+
Upload your local `.env` file to a realtime database:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Push from default .env file
|
|
90
|
+
$ dotenvrtdb --push https://your-project.firebaseio.com/env.json
|
|
91
|
+
|
|
92
|
+
# Push from custom file using -e flag
|
|
93
|
+
$ dotenvrtdb --push https://your-project.firebaseio.com/env.json -e .env.production
|
|
94
|
+
|
|
95
|
+
# Or specify -e flag before --push
|
|
96
|
+
$ dotenvrtdb -e .env.staging --push https://your-project.firebaseio.com/env.json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Example workflow:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Pull production env from Firebase
|
|
103
|
+
$ dotenvrtdb --pull https://myapp.firebaseio.com/env/prod.json -e .env.production
|
|
104
|
+
|
|
105
|
+
# Run your app with production env
|
|
106
|
+
$ dotenvrtdb -e .env.production -- node app.js
|
|
107
|
+
|
|
108
|
+
# Update local env and push back
|
|
109
|
+
$ dotenvrtdb --push https://myapp.firebaseio.com/env/prod.json -e .env.production
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Custom .env files
|
|
113
|
+
|
|
114
|
+
Another .env file could be specified using the -e flag (this will replace loading `.env` file):
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
$ dotenvrtdb -e .env2 -- <command with arguments>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Multiple .env files can be specified, and will be processed in order, but only sets variables if they haven't already been set. So the first one wins (existing env variables win over the first file and the first file wins over the second file):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
$ dotenvrtdb -e .env3 -e .env4 -- <command with arguments>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Cascading env variables
|
|
127
|
+
|
|
128
|
+
Some applications load env variables from multiple `.env` files depending on the environment:
|
|
129
|
+
|
|
130
|
+
- `.env`
|
|
131
|
+
- `.env.local`
|
|
132
|
+
- `.env.development`
|
|
133
|
+
- `.env.development.local`
|
|
134
|
+
|
|
135
|
+
dotenvrtdb supports this using the `-c` flag:
|
|
136
|
+
|
|
137
|
+
- `-c` loads `.env` and `.env.local`
|
|
138
|
+
- `-c test` loads `.env`, `.env.local`, `.env.test`, and `.env.test.local`
|
|
139
|
+
|
|
140
|
+
The `-c` flag can be used together with the `-e` flag. The following example will cascade env files located one folder up in the directory tree (`../.env` followed by `../.env.local`):
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
dotenvrtdb -e ../.env -c
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Setting variable from command line
|
|
147
|
+
|
|
148
|
+
It is possible to set variable directly from command line using the -v flag:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
$ dotenvrtdb -v VARIABLE=somevalue -- <command with arguments>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Multiple variables can be specified:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
$ dotenvrtdb -v VARIABLE1=somevalue1 -v VARIABLE2=somevalue2 -- <command with arguments>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Variables set up from command line have higher priority than from env files.
|
|
161
|
+
|
|
162
|
+
> Purpose of this is that standard approach `VARIABLE=somevalue <command with arguments>` doesn't work on Windows. The -v flag works on all the platforms.
|
|
163
|
+
|
|
164
|
+
### Check env variable
|
|
165
|
+
|
|
166
|
+
If you want to check the value of an environment variable, use the `-p` flag
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
$ dotenvrtdb -p NODE_ENV
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Flags to the underlying command
|
|
173
|
+
|
|
174
|
+
If you want to pass flags to the inner command use `--` after all the flags to `dotenvrtdb`.
|
|
175
|
+
|
|
176
|
+
E.g. the following command without dotenvrtdb:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
mvn exec:java -Dexec.args="-g -f"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
will become the following command with dotenvrtdb:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
$ dotenvrtdb -- mvn exec:java -Dexec.args="-g -f"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
or in case the env file is at `.my-env`
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
$ dotenvrtdb -e .my-env -- mvn exec:java -Dexec.args="-g -f"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Variable expansion
|
|
195
|
+
|
|
196
|
+
We support expanding env variables inside .env files (See [dotenv-expand](https://github.com/motdotla/dotenv-expand) npm package for more information)
|
|
197
|
+
|
|
198
|
+
For example:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
IP=127.0.0.1
|
|
202
|
+
PORT=1234
|
|
203
|
+
APP_URL=http://${IP}:${PORT}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Using the above example `.env` file, `process.env.APP_URL` would be `http://127.0.0.1:1234`.
|
|
207
|
+
|
|
208
|
+
#### Disabling variable expansion
|
|
209
|
+
|
|
210
|
+
If your `.env` variables include values that should not be expanded (e.g. `PASSWORD="pas$word"`), you can pass flag `--no-expand` to `dotenvrtdb` to disable variable expansion.
|
|
211
|
+
|
|
212
|
+
For example:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
dotenvrtdb --no-expand <command>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Variable expansion in the command
|
|
219
|
+
|
|
220
|
+
If your `.env` file looks like:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
SAY_HI=hello!
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
you might expect `dotenvrtdb echo "$SAY_HI"` to display `hello!`. In fact, this is not what happens: your shell will first interpret your command before passing it to `dotenvrtdb`, so if `SAY_HI` envvar is set to `""`, the command will be expanded into `dotenvrtdb echo`: that's why `dotenvrtdb` cannot make the expansion you expect.
|
|
227
|
+
|
|
228
|
+
#### Possible solutions
|
|
229
|
+
|
|
230
|
+
1. Bash and escape
|
|
231
|
+
|
|
232
|
+
One possible way to get the desired result is:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
$ dotenvrtdb -- bash -c 'echo "$SAY_HI"'
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
In bash, everything between `'` is not interpreted but passed as is. Since `$SAY_HI` is inside `''` brackets, it's passed as a string literal.
|
|
239
|
+
|
|
240
|
+
Therefore, `dotenvrtdb` will start a child process `bash -c 'echo "$SAY_HI"'` with the env variable `SAY_HI` set correctly which means bash will run `echo "$SAY_HI"` in the right environment which will print correctly `hello`
|
|
241
|
+
|
|
242
|
+
2. Subscript encapsulation
|
|
243
|
+
|
|
244
|
+
Another solution is simply to encapsulate your script in another subscript.
|
|
245
|
+
|
|
246
|
+
Example here with npm scripts in a package.json
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"scripts": {
|
|
251
|
+
"_print-stuff": "echo $STUFF",
|
|
252
|
+
"print-stuff": "dotenvrtdb -- npm run _print-stuff"
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
This example is used in a project setting (has a package.json). Should always install locally `npm install -D @tolaptrinhdh61-spec/dotenvrtdb`
|
|
258
|
+
|
|
259
|
+
### Debugging
|
|
260
|
+
|
|
261
|
+
You can add the `--debug` flag to output the `.env` files that would be processed and exit.
|
|
262
|
+
|
|
263
|
+
### Override
|
|
264
|
+
|
|
265
|
+
Override any environment variables that have already been set on your machine with values from your .env file.
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
dotenvrtdb -e .env.test -o -- jest
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Command Reference
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
Usage: dotenvrtdb [--help] [--debug] [--quiet=false] [-e <path>] [-v <n>=<value>]
|
|
275
|
+
[-p <variable name>] [-c [environment]] [--no-expand] [-- command]
|
|
276
|
+
|
|
277
|
+
Options:
|
|
278
|
+
--help print help
|
|
279
|
+
--debug output the files that would be processed but don't actually parse them
|
|
280
|
+
--quiet, -q suppress debug output from dotenv (default: true)
|
|
281
|
+
-e <path> parses the file <path> as a `.env` file
|
|
282
|
+
-e <path> multiple -e flags are allowed
|
|
283
|
+
-v <n>=<value> put variable <n> into environment using value <value>
|
|
284
|
+
-v <n>=<value> multiple -v flags are allowed
|
|
285
|
+
-p <variable> print value of <variable> to the console
|
|
286
|
+
-c [environment] support cascading env variables from multiple files
|
|
287
|
+
--no-expand skip variable expansion
|
|
288
|
+
-o, --override override system variables. Cannot be used with cascade (-c)
|
|
289
|
+
command command to run with environment variables loaded
|
|
290
|
+
|
|
291
|
+
Remote database commands:
|
|
292
|
+
--pull <url> pull env variables from remote database URL and save to file
|
|
293
|
+
use with -e flag to specify output file (default: .env)
|
|
294
|
+
example: dotenvrtdb --pull <url> -e .env.production
|
|
295
|
+
--push <url> push local .env file to remote database URL
|
|
296
|
+
use with -e flag to specify source file (default: .env)
|
|
297
|
+
example: dotenvrtdb --push <url> -e .env.staging
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Use Cases
|
|
301
|
+
|
|
302
|
+
### Team Environment Sync
|
|
303
|
+
|
|
304
|
+
Keep your team's environment variables in sync using Firebase Realtime Database:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
# Team lead pushes the base config
|
|
308
|
+
$ dotenvrtdb --push https://team-project.firebaseio.com/env/base.json
|
|
309
|
+
|
|
310
|
+
# Team members pull the config
|
|
311
|
+
$ dotenvrtdb --pull https://team-project.firebaseio.com/env/base.json
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Multi-Environment Deployment
|
|
315
|
+
|
|
316
|
+
Manage different environments easily:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Pull production config
|
|
320
|
+
$ dotenvrtdb --pull https://myapp.firebaseio.com/prod.json -e .env.production
|
|
321
|
+
|
|
322
|
+
# Pull staging config
|
|
323
|
+
$ dotenvrtdb --pull https://myapp.firebaseio.com/staging.json -e .env.staging
|
|
324
|
+
|
|
325
|
+
# Run with specific environment
|
|
326
|
+
$ dotenvrtdb -e .env.production -- node server.js
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### CI/CD Integration
|
|
330
|
+
|
|
331
|
+
Store secrets in Firebase and pull them during deployment:
|
|
332
|
+
|
|
333
|
+
```yaml
|
|
334
|
+
# .github/workflows/deploy.yml
|
|
335
|
+
- name: Pull environment variables
|
|
336
|
+
run: |
|
|
337
|
+
npm install -g @tolaptrinhdh61-spec/dotenvrtdb
|
|
338
|
+
dotenvrtdb --pull ${{ secrets.FIREBASE_ENV_URL }} -e .env.production
|
|
339
|
+
|
|
340
|
+
- name: Deploy application
|
|
341
|
+
run: dotenvrtdb -e .env.production -- npm run deploy
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Development Workflow
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
# Developer pulls latest shared config
|
|
348
|
+
$ dotenvrtdb --pull https://dev-db.firebaseio.com/config.json -e .env.development
|
|
349
|
+
|
|
350
|
+
# Make local changes and test
|
|
351
|
+
$ dotenvrtdb -e .env.development -- npm run dev
|
|
352
|
+
|
|
353
|
+
# Push updated config back (if authorized)
|
|
354
|
+
$ dotenvrtdb --push https://dev-db.firebaseio.com/config.json -e .env.development
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Remote Database Format
|
|
358
|
+
|
|
359
|
+
The remote database should return JSON in the following format:
|
|
360
|
+
|
|
361
|
+
```json
|
|
362
|
+
{
|
|
363
|
+
"DATABASE_URL": "postgresql://localhost:5432/mydb",
|
|
364
|
+
"API_KEY": "your-api-key-here",
|
|
365
|
+
"NODE_ENV": "production",
|
|
366
|
+
"PORT": "3000"
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
This will be converted to `.env` format:
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
DATABASE_URL=postgresql://localhost:5432/mydb
|
|
374
|
+
API_KEY=your-api-key-here
|
|
375
|
+
NODE_ENV=production
|
|
376
|
+
PORT=3000
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Security Features
|
|
380
|
+
|
|
381
|
+
### 🔒 Automatic Auth Token Masking
|
|
382
|
+
|
|
383
|
+
dotenvrtdb automatically masks sensitive information in URLs when displaying console output:
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
# Your command
|
|
387
|
+
$ dotenvrtdb --pull "https://myapp.firebaseio.com/env.json?auth=AIzaSyAbc123XYZ"
|
|
388
|
+
|
|
389
|
+
# Console output (auth token is masked)
|
|
390
|
+
Pulling environment variables from https://myapp.firebaseio.com/env.json?auth=******...
|
|
391
|
+
✓ Successfully pulled environment variables to .env
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Masked parameters include:
|
|
395
|
+
|
|
396
|
+
- `auth`
|
|
397
|
+
- `token`
|
|
398
|
+
- `key`
|
|
399
|
+
- `secret`
|
|
400
|
+
- `apikey`
|
|
401
|
+
- `api_key`
|
|
402
|
+
|
|
403
|
+
Username and password in URLs are also automatically masked:
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
https://user:password@example.com → https://******:******@example.com
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### ⚠️ Important Security Notes
|
|
410
|
+
|
|
411
|
+
- Never commit `.env` files containing sensitive data to version control
|
|
412
|
+
- Use Firebase Security Rules to restrict access to your env database
|
|
413
|
+
- For production secrets, consider using environment-specific databases with proper authentication
|
|
414
|
+
- The `--pull` command requires read access to the database URL
|
|
415
|
+
- The `--push` command requires write access to the database URL
|
|
416
|
+
|
|
417
|
+
Example Firebase Security Rules:
|
|
418
|
+
|
|
419
|
+
```json
|
|
420
|
+
{
|
|
421
|
+
"rules": {
|
|
422
|
+
"env": {
|
|
423
|
+
".read": "auth != null",
|
|
424
|
+
".write": "auth != null && auth.token.admin === true"
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Supported Databases
|
|
431
|
+
|
|
432
|
+
dotenvrtdb works with any HTTP/HTTPS endpoint that:
|
|
433
|
+
|
|
434
|
+
- Returns JSON in key-value format (for pull)
|
|
435
|
+
- Accepts JSON via PUT/POST request (for push)
|
|
436
|
+
|
|
437
|
+
### Compatible services:
|
|
438
|
+
|
|
439
|
+
- ✅ Firebase Realtime Database
|
|
440
|
+
- ✅ Custom REST APIs
|
|
441
|
+
- ✅ Any HTTP/HTTPS JSON endpoint
|
|
442
|
+
- ✅ Cloud Functions
|
|
443
|
+
- ✅ Serverless endpoints
|
|
444
|
+
|
|
445
|
+
## Migration from dotenv-cli
|
|
446
|
+
|
|
447
|
+
If you're currently using `dotenv-cli`, you can switch to `dotenvrtdb` with zero breaking changes:
|
|
448
|
+
|
|
449
|
+
```bash
|
|
450
|
+
# Replace
|
|
451
|
+
$ dotenv -- node app.js
|
|
452
|
+
|
|
453
|
+
# With
|
|
454
|
+
$ dotenvrtdb -- node app.js
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
All existing flags and features work exactly the same way!
|
|
458
|
+
|
|
459
|
+
## Examples
|
|
460
|
+
|
|
461
|
+
### Basic Examples
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
# Load .env and run node app
|
|
465
|
+
$ dotenvrtdb -- node app.js
|
|
466
|
+
|
|
467
|
+
# Load custom env file
|
|
468
|
+
$ dotenvrtdb -e .env.local -- npm start
|
|
469
|
+
|
|
470
|
+
# Print environment variable
|
|
471
|
+
$ dotenvrtdb -p DATABASE_URL
|
|
472
|
+
|
|
473
|
+
# Set variables from command line
|
|
474
|
+
$ dotenvrtdb -v PORT=3000 -v HOST=localhost -- node server.js
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Remote Database Examples
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
# Pull from Firebase with auth token
|
|
481
|
+
$ dotenvrtdb --pull "https://myapp.firebaseio.com/config.json?auth=YOUR_TOKEN" -e .env
|
|
482
|
+
|
|
483
|
+
# Push to custom API endpoint
|
|
484
|
+
$ dotenvrtdb --push "https://api.myapp.com/env" -e .env.production
|
|
485
|
+
|
|
486
|
+
# Pull and immediately use
|
|
487
|
+
$ dotenvrtdb --pull "https://myapp.firebaseio.com/env.json?auth=TOKEN" -e .env.temp && \
|
|
488
|
+
dotenvrtdb -e .env.temp -- node app.js
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Advanced Examples
|
|
492
|
+
|
|
493
|
+
```bash
|
|
494
|
+
# Cascade with remote sync
|
|
495
|
+
$ dotenvrtdb --pull "https://firebase.com/base.json" -e .env
|
|
496
|
+
$ dotenvrtdb -e .env -c production -- node app.js
|
|
497
|
+
|
|
498
|
+
# Multiple env files with priority
|
|
499
|
+
$ dotenvrtdb -e .env.local -e .env.shared -- npm run build
|
|
500
|
+
|
|
501
|
+
# Override system variables
|
|
502
|
+
$ dotenvrtdb -e .env.test -o -- jest
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Package Information
|
|
506
|
+
|
|
507
|
+
- **Package**: `@tolaptrinhdh61-spec/dotenvrtdb`
|
|
508
|
+
- **Command**: `dotenvrtdb`
|
|
509
|
+
- **Version**: 11.0.3
|
|
510
|
+
- **Repository**: https://github.com/tolaptrinhdh61-spec/dotenvrtdb
|
|
511
|
+
|
|
512
|
+
## Contributing
|
|
513
|
+
|
|
514
|
+
Issues and pull requests are welcome! Please visit our [GitHub repository](https://github.com/tolaptrinhdh61-spec/dotenvrtdb).
|
|
515
|
+
|
|
516
|
+
## License
|
|
517
|
+
|
|
518
|
+
[MIT](https://en.wikipedia.org/wiki/MIT_License)
|
|
519
|
+
|
|
520
|
+
## Credits
|
|
521
|
+
|
|
522
|
+
Based on [dotenv-cli](https://github.com/entropitor/dotenv-cli) with added remote database synchronization features.
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
Made with ❤️ by [tolaptrinhdh61-spec](https://github.com/tolaptrinhdh61-spec)
|
package/cli.js
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const spawn = require("cross-spawn");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const http = require("http");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
|
|
10
|
+
// ✅ FIX: Custom parsing để handle URLs bị split bởi shell hoặc masked bởi GitHub Actions
|
|
11
|
+
// Case 1: URL không có quotes, shell tách thành nhiều args
|
|
12
|
+
// VD: -eUrl https://example.com?auth=123&key=456
|
|
13
|
+
// Shell tách: ['-eUrl', 'https://example.com?auth=123', 'key=456']
|
|
14
|
+
// → Ghép lại: ['-eUrl', 'https://example.com?auth=123&key=456']
|
|
15
|
+
//
|
|
16
|
+
// Case 2: GitHub Actions mask secret thành ***, không có quotes
|
|
17
|
+
// VD: -eUrl *** -- node script.js
|
|
18
|
+
// GitHub mask trước: ['-eUrl', '***', '--', 'node', 'script.js']
|
|
19
|
+
// → Giữ nguyên '***' và tìm '--' để tách command
|
|
20
|
+
function parseArguments(args) {
|
|
21
|
+
const result = [];
|
|
22
|
+
let i = 0;
|
|
23
|
+
|
|
24
|
+
// Tìm vị trí của '--' trước tiên
|
|
25
|
+
const doubleDashIndex = args.indexOf("--");
|
|
26
|
+
|
|
27
|
+
while (i < args.length) {
|
|
28
|
+
const arg = args[i];
|
|
29
|
+
|
|
30
|
+
// Nếu gặp '--', dừng parsing flags và pass everything sau đó as command
|
|
31
|
+
if (arg === "--") {
|
|
32
|
+
// Không push '--' vào result, nhưng push tất cả args sau nó
|
|
33
|
+
i++;
|
|
34
|
+
while (i < args.length) {
|
|
35
|
+
result.push(args[i]);
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Nếu là -eUrl, cần xử lý đặc biệt
|
|
42
|
+
if (arg === "-eUrl" || arg === "--eUrl") {
|
|
43
|
+
result.push(arg);
|
|
44
|
+
i++;
|
|
45
|
+
|
|
46
|
+
// Ghép tất cả args tiếp theo cho đến khi gặp '--' hoặc arg bắt đầu bằng '-'
|
|
47
|
+
let urlParts = [];
|
|
48
|
+
while (i < args.length && args[i] !== "--") {
|
|
49
|
+
const nextArg = args[i];
|
|
50
|
+
|
|
51
|
+
// Nếu gặp flag khác (bắt đầu với '-' nhưng KHÔNG phải '***' hoặc pattern giống)
|
|
52
|
+
// thì dừng lại
|
|
53
|
+
if (nextArg.startsWith("-") && !nextArg.match(/^-[\*\+\.]+$/)) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
urlParts.push(nextArg);
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ghép lại thành 1 URL với '&' (trường hợp shell tách bởi &)
|
|
62
|
+
// Hoặc giữ nguyên nếu là masked value như '***'
|
|
63
|
+
if (urlParts.length > 0) {
|
|
64
|
+
result.push(urlParts.join("&"));
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
result.push(arg);
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parsedArgs = parseArguments(process.argv.slice(2));
|
|
76
|
+
const argv = require("minimist")(parsedArgs);
|
|
77
|
+
const dotenv = require("dotenv");
|
|
78
|
+
|
|
79
|
+
// ✅ FIX: Xử lý import dotenv-expand cho cả CommonJS và ES modules
|
|
80
|
+
let dotenvExpand;
|
|
81
|
+
try {
|
|
82
|
+
// Thử import theo cách mới (dotenv-expand >= 9.0)
|
|
83
|
+
const dotenvExpandModule = require("dotenv-expand");
|
|
84
|
+
dotenvExpand = dotenvExpandModule.expand || dotenvExpandModule.default || dotenvExpandModule;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error("Failed to load dotenv-expand:", err.message);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Biến lưu danh sách các file tạm cần xóa
|
|
91
|
+
const tempFilesToCleanup = [];
|
|
92
|
+
let isCleanedUp = false; // Prevent double cleanup
|
|
93
|
+
|
|
94
|
+
function printHelp() {
|
|
95
|
+
console.log(
|
|
96
|
+
[
|
|
97
|
+
"Usage: dotenvrtdb [--help] [--debug] [--quiet=false] [-e <path>] [-eUrl <url>] [-v <n>=<value>] [-p <variable name>] [-c [environment]] [--no-expand] [-- command]",
|
|
98
|
+
" --help print help",
|
|
99
|
+
" --debug output the files that would be processed but don't actually parse them or run the `command`",
|
|
100
|
+
" --quiet, -q suppress debug output from dotenv (default: true)",
|
|
101
|
+
" -e <path> parses the file <path> as a `.env` file and adds the variables to the environment",
|
|
102
|
+
" -e <path> multiple -e flags are allowed",
|
|
103
|
+
" -eUrl <url> pull env from remote URL to temp file, use it, then delete temp file",
|
|
104
|
+
" -eUrl <url> multiple -eUrl flags are allowed",
|
|
105
|
+
" -v <n>=<value> put variable <n> into environment using value <value>",
|
|
106
|
+
" -v <n>=<value> multiple -v flags are allowed",
|
|
107
|
+
" -p <variable> print value of <variable> to the console. If you specify this, you do not have to specify a `command`",
|
|
108
|
+
" -c [environment] support cascading env variables from `.env`, `.env.<environment>`, `.env.local`, `.env.<environment>.local` files",
|
|
109
|
+
" --no-expand skip variable expansion",
|
|
110
|
+
" -o, --override override system variables. Cannot be used along with cascade (-c).",
|
|
111
|
+
" command `command` is the actual command you want to run. Best practice is to precede this command with ` -- `. Everything after `--` is considered to be your command. So any flags will not be parsed by this tool but be passed to your command. If you do not do it, this tool will strip those flags",
|
|
112
|
+
"",
|
|
113
|
+
"Remote database commands:",
|
|
114
|
+
" --pull <url> pull env variables from remote database URL and save to file",
|
|
115
|
+
" use with -e flag to specify output file (default: .env)",
|
|
116
|
+
" example: dotenvrtdb --pull <url> -e .env.production",
|
|
117
|
+
" --push <url> push local .env file to remote database URL",
|
|
118
|
+
" use with -e flag to specify source file (default: .env)",
|
|
119
|
+
" example: dotenvrtdb --push <url> -e .env.staging",
|
|
120
|
+
].join("\n"),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Hàm cleanup để xóa các file tạm
|
|
125
|
+
function cleanupTempFiles() {
|
|
126
|
+
if (isCleanedUp) return;
|
|
127
|
+
isCleanedUp = true;
|
|
128
|
+
|
|
129
|
+
if (tempFilesToCleanup.length === 0) {
|
|
130
|
+
return; // Không có file tạm nào để xóa
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const isDebug = argv.debug;
|
|
134
|
+
const isQuiet = !(argv.quiet === false || argv.q === false || argv.quiet === "false" || argv.q === "false");
|
|
135
|
+
|
|
136
|
+
// 🔒 ALWAYS show cleanup message for security awareness
|
|
137
|
+
if (!isQuiet) {
|
|
138
|
+
console.log(`\n🧹 Cleaning up ${tempFilesToCleanup.length} temporary file(s)...`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let deletedCount = 0;
|
|
142
|
+
let failedCount = 0;
|
|
143
|
+
|
|
144
|
+
tempFilesToCleanup.forEach((filePath) => {
|
|
145
|
+
try {
|
|
146
|
+
if (fs.existsSync(filePath)) {
|
|
147
|
+
fs.unlinkSync(filePath);
|
|
148
|
+
deletedCount++;
|
|
149
|
+
if (isDebug || !isQuiet) {
|
|
150
|
+
console.log(` ✓ Deleted: ${path.basename(filePath)}`);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
if (isDebug) {
|
|
154
|
+
console.log(` ⊘ Already deleted: ${path.basename(filePath)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
failedCount++;
|
|
159
|
+
console.error(` ✗ Failed to delete ${path.basename(filePath)}: ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!isQuiet && deletedCount > 0) {
|
|
164
|
+
console.log(`✓ Successfully deleted ${deletedCount} temporary file(s)\n`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (failedCount > 0) {
|
|
168
|
+
console.error(`⚠️ Warning: ${failedCount} file(s) could not be deleted. Please check manually.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Đăng ký cleanup khi process kết thúc
|
|
173
|
+
process.on("exit", cleanupTempFiles);
|
|
174
|
+
process.on("SIGINT", () => {
|
|
175
|
+
cleanupTempFiles();
|
|
176
|
+
process.exit(130);
|
|
177
|
+
});
|
|
178
|
+
process.on("SIGTERM", () => {
|
|
179
|
+
cleanupTempFiles();
|
|
180
|
+
process.exit(143);
|
|
181
|
+
});
|
|
182
|
+
process.on("uncaughtException", (err) => {
|
|
183
|
+
console.error("Uncaught exception:", err);
|
|
184
|
+
cleanupTempFiles();
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
|
187
|
+
process.on("unhandledRejection", (err) => {
|
|
188
|
+
console.error("Unhandled rejection:", err);
|
|
189
|
+
cleanupTempFiles();
|
|
190
|
+
process.exit(1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Hàm mask URL để ẩn auth token
|
|
194
|
+
function maskUrl(url) {
|
|
195
|
+
try {
|
|
196
|
+
const urlObj = new URL(url);
|
|
197
|
+
|
|
198
|
+
// Mask query parameters chứa auth/token/key
|
|
199
|
+
const params = new URLSearchParams(urlObj.search);
|
|
200
|
+
const maskedParams = new URLSearchParams();
|
|
201
|
+
|
|
202
|
+
for (const [key, value] of params.entries()) {
|
|
203
|
+
const lowerKey = key.toLowerCase();
|
|
204
|
+
if (lowerKey.includes("auth") || lowerKey.includes("token") || lowerKey.includes("key") || lowerKey.includes("secret")) {
|
|
205
|
+
maskedParams.set(key, "******");
|
|
206
|
+
} else {
|
|
207
|
+
maskedParams.set(key, value);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
urlObj.search = maskedParams.toString();
|
|
212
|
+
|
|
213
|
+
// Mask username/password trong URL
|
|
214
|
+
if (urlObj.username || urlObj.password) {
|
|
215
|
+
urlObj.username = urlObj.username ? "******" : "";
|
|
216
|
+
urlObj.password = urlObj.password ? "******" : "";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return urlObj.toString();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// Nếu không parse được URL, mask theo pattern
|
|
222
|
+
return url.replace(/([?&])(auth|token|key|secret|apikey|api_key)=([^&]+)/gi, "$1$2=******").replace(/\/\/([^:]+):([^@]+)@/gi, "//******:******@");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Hàm fetch dữ liệu từ URL
|
|
227
|
+
function fetchFromUrl(url) {
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
const protocol = url.startsWith("https") ? https : http;
|
|
230
|
+
|
|
231
|
+
protocol
|
|
232
|
+
.get(url, (res) => {
|
|
233
|
+
let data = "";
|
|
234
|
+
|
|
235
|
+
if (res.statusCode !== 200) {
|
|
236
|
+
reject(new Error(`Failed to fetch from ${maskUrl(url)}. Status code: ${res.statusCode}`));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
res.on("data", (chunk) => {
|
|
241
|
+
data += chunk;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
res.on("end", () => {
|
|
245
|
+
try {
|
|
246
|
+
const jsonData = JSON.parse(data);
|
|
247
|
+
resolve(jsonData);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
reject(new Error(`Failed to parse JSON from ${maskUrl(url)}: ${err.message}`));
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
})
|
|
253
|
+
.on("error", (err) => {
|
|
254
|
+
reject(new Error(`Failed to fetch from ${maskUrl(url)}: ${err.message}`));
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Hàm push dữ liệu lên URL (PUT request for Firebase Realtime Database)
|
|
260
|
+
function pushToUrl(url, data) {
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
const urlObj = new URL(url);
|
|
263
|
+
const protocol = url.startsWith("https") ? https : http;
|
|
264
|
+
|
|
265
|
+
const jsonData = JSON.stringify(data);
|
|
266
|
+
|
|
267
|
+
const options = {
|
|
268
|
+
hostname: urlObj.hostname,
|
|
269
|
+
port: urlObj.port,
|
|
270
|
+
path: urlObj.pathname + urlObj.search,
|
|
271
|
+
method: "PUT",
|
|
272
|
+
headers: {
|
|
273
|
+
"Content-Type": "application/json",
|
|
274
|
+
"Content-Length": Buffer.byteLength(jsonData),
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const req = protocol.request(options, (res) => {
|
|
279
|
+
let responseData = "";
|
|
280
|
+
|
|
281
|
+
res.on("data", (chunk) => {
|
|
282
|
+
responseData += chunk;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
res.on("end", () => {
|
|
286
|
+
if (res.statusCode === 200 || res.statusCode === 201) {
|
|
287
|
+
resolve(responseData);
|
|
288
|
+
} else {
|
|
289
|
+
reject(new Error(`Failed to push to ${maskUrl(url)}. Status code: ${res.statusCode}`));
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
req.on("error", (err) => {
|
|
295
|
+
reject(new Error(`Failed to push to ${maskUrl(url)}: ${err.message}`));
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
req.write(jsonData);
|
|
299
|
+
req.end();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Hàm chuyển đổi object thành format .env
|
|
304
|
+
function objectToEnvFormat(obj) {
|
|
305
|
+
if (!obj || typeof obj !== "object") {
|
|
306
|
+
return "";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return Object.entries(obj)
|
|
310
|
+
.map(([key, value]) => {
|
|
311
|
+
// Escape giá trị nếu chứa ký tự đặc biệt
|
|
312
|
+
const stringValue = String(value);
|
|
313
|
+
if (stringValue.includes("\n") || stringValue.includes('"') || stringValue.includes(" ")) {
|
|
314
|
+
return `${key}="${stringValue.replace(/"/g, '\\"')}"`;
|
|
315
|
+
}
|
|
316
|
+
return `${key}=${stringValue}`;
|
|
317
|
+
})
|
|
318
|
+
.join("\n");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Hàm đọc file .env và chuyển thành object
|
|
322
|
+
function parseEnvFile(filePath) {
|
|
323
|
+
try {
|
|
324
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
325
|
+
const lines = content.split("\n");
|
|
326
|
+
const result = {};
|
|
327
|
+
|
|
328
|
+
for (const line of lines) {
|
|
329
|
+
const trimmedLine = line.trim();
|
|
330
|
+
|
|
331
|
+
// Bỏ qua comment và dòng trống
|
|
332
|
+
if (!trimmedLine || trimmedLine.startsWith("#")) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Parse key=value
|
|
337
|
+
const match = trimmedLine.match(/^([^=]+)=(.*)$/);
|
|
338
|
+
if (match) {
|
|
339
|
+
const key = match[1].trim();
|
|
340
|
+
let value = match[2].trim();
|
|
341
|
+
|
|
342
|
+
// Xử lý giá trị trong dấu ngoặc kép
|
|
343
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
344
|
+
value = value.slice(1, -1).replace(/\\"/g, '"');
|
|
345
|
+
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
346
|
+
value = value.slice(1, -1);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
result[key] = value;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return result;
|
|
354
|
+
} catch (err) {
|
|
355
|
+
throw new Error(`Failed to read or parse ${filePath}: ${err.message}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Hàm tạo file tạm từ URL
|
|
360
|
+
async function createTempFileFromUrl(url, index = 0) {
|
|
361
|
+
const isDebug = argv.debug;
|
|
362
|
+
const isQuiet = !(argv.quiet === false || argv.q === false || argv.quiet === "false" || argv.q === "false");
|
|
363
|
+
const timestamp = Date.now();
|
|
364
|
+
const randomSuffix = Math.random().toString(36).substring(7);
|
|
365
|
+
const tempFileName = `.env.temp.${timestamp}.${index}.${randomSuffix}`;
|
|
366
|
+
|
|
367
|
+
// 🔒 CRITICAL SECURITY FIX: Tạo temp file trong OS temp directory
|
|
368
|
+
// KHÔNG tạo trong cwd để tránh:
|
|
369
|
+
// 1. File bị commit vào git
|
|
370
|
+
// 2. File bị publish lên npm package
|
|
371
|
+
// 3. Secrets bị leak ra public
|
|
372
|
+
const tempFilePath = path.join(os.tmpdir(), tempFileName);
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
if (isDebug || !isQuiet) {
|
|
376
|
+
console.log(`📥 Fetching env vars from ${maskUrl(url)}...`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const data = await fetchFromUrl(url);
|
|
380
|
+
const envContent = objectToEnvFormat(data);
|
|
381
|
+
|
|
382
|
+
fs.writeFileSync(tempFilePath, envContent, "utf-8");
|
|
383
|
+
|
|
384
|
+
if (isDebug || !isQuiet) {
|
|
385
|
+
console.log(` ✓ Created temp file: ${tempFileName}`);
|
|
386
|
+
console.log(` 📍 Location: ${os.tmpdir()}`);
|
|
387
|
+
console.log(` 🔒 Will be auto-deleted after execution`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Thêm vào danh sách cần cleanup
|
|
391
|
+
tempFilesToCleanup.push(tempFilePath);
|
|
392
|
+
|
|
393
|
+
return tempFilePath;
|
|
394
|
+
} catch (err) {
|
|
395
|
+
throw new Error(`Failed to create temp file from ${maskUrl(url)}: ${err.message}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Xử lý lệnh pull
|
|
400
|
+
async function handlePull(url, outputPath) {
|
|
401
|
+
try {
|
|
402
|
+
console.log(`Pulling environment variables from ${maskUrl(url)}...`);
|
|
403
|
+
const data = await fetchFromUrl(url);
|
|
404
|
+
const envContent = objectToEnvFormat(data);
|
|
405
|
+
|
|
406
|
+
fs.writeFileSync(outputPath, envContent, "utf-8");
|
|
407
|
+
console.log(`✓ Successfully pulled environment variables to ${outputPath}`);
|
|
408
|
+
process.exit(0);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
console.error(`✗ Pull failed: ${err.message}`);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Xử lý lệnh push
|
|
416
|
+
async function handlePush(url, sourcePath) {
|
|
417
|
+
try {
|
|
418
|
+
console.log(`Pushing environment variables from ${sourcePath} to ${maskUrl(url)}...`);
|
|
419
|
+
|
|
420
|
+
if (!fs.existsSync(sourcePath)) {
|
|
421
|
+
throw new Error(`Source file ${sourcePath} does not exist`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const envData = parseEnvFile(sourcePath);
|
|
425
|
+
await pushToUrl(url, envData);
|
|
426
|
+
|
|
427
|
+
console.log(`✓ Successfully pushed environment variables to ${maskUrl(url)}`);
|
|
428
|
+
process.exit(0);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.error(`✗ Push failed: ${err.message}`);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Xử lý -eUrl: Pull từ URL vào file tạm
|
|
436
|
+
async function processEUrlFlags() {
|
|
437
|
+
// ✅ FIX: Support reading URL from environment variable
|
|
438
|
+
// Nếu -eUrl = "***" (GitHub Actions masked), thử đọc từ env var
|
|
439
|
+
// Priority: 1) argv.eUrl nếu không phải "***", 2) DOTENVRTDB_URL env var
|
|
440
|
+
|
|
441
|
+
let urls = [];
|
|
442
|
+
|
|
443
|
+
if (argv.eUrl) {
|
|
444
|
+
let eUrlValue = typeof argv.eUrl === "string" ? [argv.eUrl] : argv.eUrl;
|
|
445
|
+
|
|
446
|
+
if (!Array.isArray(eUrlValue)) {
|
|
447
|
+
eUrlValue = [eUrlValue];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Check if URL is masked by GitHub Actions
|
|
451
|
+
urls = eUrlValue.map((url) => {
|
|
452
|
+
if (url === "***" || url === "*" || url.match(/^\*+$/)) {
|
|
453
|
+
// URL was masked, try to get from environment variable
|
|
454
|
+
const envUrl = process.env.DOTENVRTDB_URL;
|
|
455
|
+
if (envUrl) {
|
|
456
|
+
console.log("⚠️ Detected masked URL (***), using DOTENVRTDB_URL environment variable");
|
|
457
|
+
return envUrl;
|
|
458
|
+
} else {
|
|
459
|
+
console.error("❌ URL was masked by GitHub Actions but DOTENVRTDB_URL env var not found");
|
|
460
|
+
console.error(" Please set DOTENVRTDB_URL as environment variable:");
|
|
461
|
+
console.error(" env:");
|
|
462
|
+
console.error(" DOTENVRTDB_URL: ${{ secrets.DOTENVRTDB_URL }}");
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return url;
|
|
467
|
+
});
|
|
468
|
+
} else if (process.env.DOTENVRTDB_URL) {
|
|
469
|
+
// Không có -eUrl flag nhưng có env var
|
|
470
|
+
urls = [process.env.DOTENVRTDB_URL];
|
|
471
|
+
console.log("ℹ️ Using DOTENVRTDB_URL from environment variable");
|
|
472
|
+
} else {
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
urls = urls.filter((url) => url && typeof url === "string" && url.trim().length > 0);
|
|
477
|
+
|
|
478
|
+
if (urls.length === 0) {
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const tempPaths = [];
|
|
483
|
+
|
|
484
|
+
for (let i = 0; i < urls.length; i++) {
|
|
485
|
+
const url = urls[i];
|
|
486
|
+
try {
|
|
487
|
+
const tempPath = await createTempFileFromUrl(url, i);
|
|
488
|
+
tempPaths.push(tempPath);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
console.error(`✗ Failed to process -eUrl ${maskUrl(url)}: ${err.message}`);
|
|
491
|
+
cleanupTempFiles();
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return tempPaths;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Main async function để xử lý -eUrl
|
|
500
|
+
async function main() {
|
|
501
|
+
const override = argv.o || argv.override;
|
|
502
|
+
|
|
503
|
+
// Handle quiet flag - default is true (quiet), can be disabled with --quiet=false or -q=false
|
|
504
|
+
const isQuiet = !(argv.quiet === false || argv.q === false || argv.quiet === "false" || argv.q === "false");
|
|
505
|
+
|
|
506
|
+
if (argv.c && override) {
|
|
507
|
+
console.error("Invalid arguments. Cascading env variables conflicts with overrides.");
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
let paths = [];
|
|
512
|
+
|
|
513
|
+
// Xử lý -eUrl trước
|
|
514
|
+
const tempPaths = await processEUrlFlags();
|
|
515
|
+
|
|
516
|
+
if (Array.isArray(tempPaths) && tempPaths.length > 0) {
|
|
517
|
+
paths.push(...tempPaths);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Sau đó xử lý -e như bình thường
|
|
521
|
+
if (argv.e) {
|
|
522
|
+
if (typeof argv.e === "string") {
|
|
523
|
+
paths.push(argv.e);
|
|
524
|
+
} else if (Array.isArray(argv.e)) {
|
|
525
|
+
paths.push(...argv.e);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Nếu không có -e và -eUrl, dùng .env mặc định
|
|
530
|
+
if (paths.length === 0) {
|
|
531
|
+
paths.push(".env");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (argv.c) {
|
|
535
|
+
paths = paths.reduce(
|
|
536
|
+
(accumulator, envPath) =>
|
|
537
|
+
accumulator.concat(
|
|
538
|
+
typeof argv.c === "string"
|
|
539
|
+
? [`${envPath}.${argv.c}.local`, `${envPath}.local`, `${envPath}.${argv.c}`, envPath]
|
|
540
|
+
: [`${envPath}.local`, envPath],
|
|
541
|
+
),
|
|
542
|
+
[],
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function validateCmdVariable(param) {
|
|
547
|
+
const [, key, val] = param.match(/^(\w+)=([\s\S]+)$/m) || [];
|
|
548
|
+
if (!key || !val) {
|
|
549
|
+
console.error(`Invalid variable name. Expected variable in format '-v variable=value', but got: \`-v ${param}\`.`);
|
|
550
|
+
cleanupTempFiles();
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return [key, val];
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const variables = [];
|
|
558
|
+
if (argv.v) {
|
|
559
|
+
if (typeof argv.v === "string") {
|
|
560
|
+
variables.push(validateCmdVariable(argv.v));
|
|
561
|
+
} else {
|
|
562
|
+
variables.push(...argv.v.map(validateCmdVariable));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const parsedVariables = Object.fromEntries(variables);
|
|
566
|
+
|
|
567
|
+
if (argv.debug) {
|
|
568
|
+
console.log("Files to be processed:");
|
|
569
|
+
console.log(paths);
|
|
570
|
+
console.log("\nVariables from command line:");
|
|
571
|
+
console.log(parsedVariables);
|
|
572
|
+
if (tempFilesToCleanup.length > 0) {
|
|
573
|
+
console.log("\nTemp files (will be deleted after execution):");
|
|
574
|
+
console.log(tempFilesToCleanup);
|
|
575
|
+
}
|
|
576
|
+
cleanupTempFiles();
|
|
577
|
+
process.exit();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ✅ FIX: Load và expand từng file với kiểm tra function
|
|
581
|
+
paths.forEach(function (env) {
|
|
582
|
+
const resolvedPath = path.resolve(env);
|
|
583
|
+
|
|
584
|
+
// Debug: Check if file exists
|
|
585
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
586
|
+
if (!isQuiet) {
|
|
587
|
+
console.warn(`Warning: File does not exist: ${resolvedPath}`);
|
|
588
|
+
}
|
|
589
|
+
return; // Skip this file
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const result = dotenv.config({ path: resolvedPath, override, quiet: isQuiet });
|
|
593
|
+
|
|
594
|
+
// Debug: Check if file was loaded successfully
|
|
595
|
+
if (result.error && !isQuiet) {
|
|
596
|
+
console.error(`Error loading ${resolvedPath}:`, result.error.message);
|
|
597
|
+
} else if (result.parsed && !isQuiet) {
|
|
598
|
+
console.log(`✓ Loaded ${Object.keys(result.parsed).length} variables from ${resolvedPath}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Expand variables nếu cần và nếu dotenvExpand là function
|
|
602
|
+
if (argv.expand !== false && result.parsed && typeof dotenvExpand === "function") {
|
|
603
|
+
dotenvExpand(result);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Thêm variables từ command line
|
|
608
|
+
Object.assign(process.env, parsedVariables);
|
|
609
|
+
|
|
610
|
+
if (argv.p) {
|
|
611
|
+
let value = process.env[argv.p];
|
|
612
|
+
if (typeof value === "string") {
|
|
613
|
+
value = `${value}`;
|
|
614
|
+
}
|
|
615
|
+
console.log(value != null ? value : "");
|
|
616
|
+
cleanupTempFiles();
|
|
617
|
+
process.exit();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const command = argv._[0];
|
|
621
|
+
|
|
622
|
+
// ✅ FIX: Nếu không có command nhưng có -eUrl hợp lệ
|
|
623
|
+
// GitHub Actions có thể mask secret thành *** khiến parsing bị lỗi
|
|
624
|
+
// Trong trường hợp này, check xem có remaining args sau khi parse không
|
|
625
|
+
if (!command && argv.eUrl) {
|
|
626
|
+
console.error("ERROR: No command provided after arguments.");
|
|
627
|
+
console.error("When using -eUrl, make sure to include the command to run.");
|
|
628
|
+
console.error("");
|
|
629
|
+
console.error("Examples:");
|
|
630
|
+
console.error(' dotenvrtdb -eUrl "${{ secrets.URL }}" -- node script.js');
|
|
631
|
+
console.error(' dotenvrtdb -eUrl "https://example.com" -- npm start');
|
|
632
|
+
console.error("");
|
|
633
|
+
console.error("Note: In GitHub Actions, always quote secrets to prevent parsing issues:");
|
|
634
|
+
console.error(' -eUrl "${{ secrets.DOTENVRTDB_URL }}" -- command');
|
|
635
|
+
console.error("");
|
|
636
|
+
printHelp();
|
|
637
|
+
cleanupTempFiles();
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (!command) {
|
|
642
|
+
printHelp();
|
|
643
|
+
cleanupTempFiles();
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const child = spawn(command, argv._.slice(1), { stdio: "inherit" }).on("exit", function (exitCode, signal) {
|
|
648
|
+
cleanupTempFiles();
|
|
649
|
+
if (typeof exitCode === "number") {
|
|
650
|
+
process.exit(exitCode);
|
|
651
|
+
} else {
|
|
652
|
+
process.kill(process.pid, signal);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGPIPE", "SIGHUP", "SIGBREAK", "SIGWINCH", "SIGUSR1", "SIGUSR2"]) {
|
|
657
|
+
process.on(signal, function () {
|
|
658
|
+
child.kill(signal);
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Entry point
|
|
664
|
+
(async function () {
|
|
665
|
+
// ✅ DEBUG: Always log raw arguments để diagnose vấn đề
|
|
666
|
+
const hasEUrl = process.argv.includes("-eUrl") || process.argv.includes("--eUrl");
|
|
667
|
+
const hasDoubleDash = process.argv.includes("--");
|
|
668
|
+
const hasCommand = argv._.length > 0;
|
|
669
|
+
|
|
670
|
+
// Nếu có -eUrl nhưng không có command, có thể là parsing issue
|
|
671
|
+
if (hasEUrl && !hasCommand && !argv.help && !argv.pull && !argv.push && !argv.debug && !argv.p) {
|
|
672
|
+
console.error("=== ARGUMENT PARSING DEBUG ===");
|
|
673
|
+
console.error("Raw process.argv:", process.argv);
|
|
674
|
+
console.error("Parsed args:", parsedArgs);
|
|
675
|
+
console.error("Minimist result:", JSON.stringify(argv, null, 2));
|
|
676
|
+
console.error("Has --:", hasDoubleDash);
|
|
677
|
+
console.error("==============================");
|
|
678
|
+
console.error("");
|
|
679
|
+
console.error("ERROR: No command provided after arguments");
|
|
680
|
+
console.error("");
|
|
681
|
+
console.error("The most common cause is forgetting '--' before the command:");
|
|
682
|
+
console.error(" ❌ Wrong: dotenvrtdb -eUrl ${{ secrets.URL }} node script.js");
|
|
683
|
+
console.error(' ✅ Correct: dotenvrtdb -eUrl "${{ secrets.URL }}" -- node script.js');
|
|
684
|
+
console.error("");
|
|
685
|
+
console.error("In GitHub Actions, ALWAYS quote secrets and use '--':");
|
|
686
|
+
console.error(' run: dotenvrtdb -eUrl "${{ secrets.DOTENVRTDB_URL }}" -- node ./bin/cli.js publish');
|
|
687
|
+
console.error("");
|
|
688
|
+
printHelp();
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// DEBUG MODE
|
|
693
|
+
const isDebugMode = argv.debug || process.env.DEBUG_DOTENVRTDB === "true";
|
|
694
|
+
if (isDebugMode) {
|
|
695
|
+
console.log("=== DEBUG MODE ===");
|
|
696
|
+
console.log("Raw process.argv:", process.argv);
|
|
697
|
+
console.log("Parsed args:", parsedArgs);
|
|
698
|
+
console.log("Minimist argv:", JSON.stringify(argv, null, 2));
|
|
699
|
+
console.log("==================");
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (argv.help) {
|
|
703
|
+
printHelp();
|
|
704
|
+
process.exit();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Xử lý lệnh pull
|
|
708
|
+
if (argv.pull) {
|
|
709
|
+
const pullUrl = argv.pull;
|
|
710
|
+
let outputPath = ".env";
|
|
711
|
+
if (argv.e) {
|
|
712
|
+
outputPath = typeof argv.e === "string" ? argv.e : argv.e[0];
|
|
713
|
+
}
|
|
714
|
+
await handlePull(pullUrl, outputPath);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Xử lý lệnh push
|
|
719
|
+
if (argv.push) {
|
|
720
|
+
const pushUrl = argv.push;
|
|
721
|
+
let sourcePath = ".env";
|
|
722
|
+
if (argv.e) {
|
|
723
|
+
sourcePath = typeof argv.e === "string" ? argv.e : argv.e[0];
|
|
724
|
+
}
|
|
725
|
+
await handlePush(pushUrl, sourcePath);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Chạy main function
|
|
730
|
+
await main();
|
|
731
|
+
})().catch((err) => {
|
|
732
|
+
console.error("Fatal error:", err.message);
|
|
733
|
+
console.error(err.stack);
|
|
734
|
+
cleanupTempFiles();
|
|
735
|
+
process.exit(1);
|
|
736
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tltdh61/dotenvrtdb",
|
|
3
|
+
"version": "1.260131.11434",
|
|
4
|
+
"description": "A CLI tool to load environment variables from .env files with remote database support",
|
|
5
|
+
"main": "cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dotenvrtdb": "./cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"publish-o861runners": "dotenv -e .env.dotenvrtdb -- npx --yes @o861runners/nmp publish --debug"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/tolaptrinhdh61-spec/dotenvrtdb.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dotenv",
|
|
18
|
+
"dotenvrtdb",
|
|
19
|
+
"env",
|
|
20
|
+
"environment",
|
|
21
|
+
"variables",
|
|
22
|
+
"config",
|
|
23
|
+
"cli",
|
|
24
|
+
"firebase",
|
|
25
|
+
"realtime-database"
|
|
26
|
+
],
|
|
27
|
+
"author": "tolaptrinhdh61-spec",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/tolaptrinhdh61-spec/dotenvrtdb/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/tolaptrinhdh61-spec/dotenvrtdb#readme",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"cross-spawn": "^7.0.3",
|
|
35
|
+
"dotenv": "^16.0.0",
|
|
36
|
+
"dotenv-expand": "^10.0.0",
|
|
37
|
+
"minimist": "^1.2.8"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"registry": "https://registry.npmjs.org/"
|
|
41
|
+
}
|
|
42
|
+
}
|