@saasak/tool-env 0.0.2 → 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/README.md +77 -0
- package/bin/index.js +236 -170
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Env management
|
|
2
|
+
|
|
3
|
+
## Warnings
|
|
4
|
+
|
|
5
|
+
- Make sure to have the git hook setup OR TO add any new variable viw the dedicated subcommand
|
|
6
|
+
- Obviously do not commit your password file
|
|
7
|
+
- In variable composition, the "static" parts are NOT encrypted (static part should never contain sensitive information)
|
|
8
|
+
|
|
9
|
+
## Goals
|
|
10
|
+
|
|
11
|
+
- Centralize env variables definitions
|
|
12
|
+
- Allow explicit variables composition
|
|
13
|
+
- Good DX (setup and forget)
|
|
14
|
+
|
|
15
|
+
## TODOs
|
|
16
|
+
|
|
17
|
+
[] Handle encryption
|
|
18
|
+
[] Handle variable composition everywhere (not just in overrides)
|
|
19
|
+
[] Create runtime library to read vars (even encrypted)
|
|
20
|
+
[] Handle env.local files
|
|
21
|
+
|
|
22
|
+
## Open questions
|
|
23
|
+
|
|
24
|
+
- Should we write all the env with the next.js convention ?
|
|
25
|
+
- How to handle adding a var easily in an encrypted context ?
|
|
26
|
+
- Should we split BUILD / RUN variables ?
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
in package.json (root)
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"scripts": {
|
|
34
|
+
"postinstall": "wrenv --secret ~/.big-secret write --target dev",
|
|
35
|
+
"env:update": "wrenv --secret ~/.big-secret write"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
and then later
|
|
41
|
+
```bash
|
|
42
|
+
pnpm run env:update --target prod
|
|
43
|
+
```
|
|
44
|
+
or
|
|
45
|
+
```bash
|
|
46
|
+
WRENV_TARGET=staging bun run env:update
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
you can also pass the secret via an env variable (Even though it is not really encouraged)
|
|
50
|
+
```bash
|
|
51
|
+
WRENV_SECRET=super-secret WRENV_TARGET=prod npm run env:update
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
or via stdin
|
|
55
|
+
```bash
|
|
56
|
+
cat ~/.big-secret | wrenv --secret=stdin write --target=dev
|
|
57
|
+
wrenv --secret stdin --target=dev < ~/.big-secret
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Git hooks
|
|
61
|
+
|
|
62
|
+
Wrenv provide a git hook (to be configured independently with the solution of your choosing) to encrypt all added variables
|
|
63
|
+
So in `.git/hooks/pre-commit` you can add
|
|
64
|
+
```bash
|
|
65
|
+
bun run wrenv --secret ~/.big-secret besafe
|
|
66
|
+
```
|
|
67
|
+
This will run on the .env.json file and make sure all variables are encrypted.
|
|
68
|
+
This way you can add new variables and make sure they don't leak, offering minimum friction.
|
|
69
|
+
|
|
70
|
+
/!\ You must make sure to add this hook OR to always add var via the dedicated subcommand `wrenv add`
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## Add a variable
|
|
74
|
+
|
|
75
|
+
To add a new variable use the `add` subcommand
|
|
76
|
+
```bash
|
|
77
|
+
wrenv --secret=~/.big-secret add NEW_VAR +fallback=@@_VALUE +dev=DEV_VALUE +production=PROD_VALUE
|
package/bin/index.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import minimist from 'minimist';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
|
|
6
|
+
import { buildEnv } from '../src/utils-env.js';
|
|
7
|
+
import { findMonorepoPackages } from '../src/utils-pkg.js';
|
|
8
|
+
import { encrypt, isEncrypted } from '../src/utils-crypto.js';
|
|
5
9
|
|
|
6
10
|
const args = minimist(process.argv.slice(2));
|
|
7
|
-
const
|
|
8
|
-
|
|
11
|
+
const __root = process.cwd();
|
|
12
|
+
|
|
13
|
+
const command = args._[0] || 'write';
|
|
14
|
+
|
|
15
|
+
const secret = args.secret
|
|
16
|
+
? (args.secret === 'stdin' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(args.secret, 'utf8'))
|
|
17
|
+
: process.env.WRENV_SECRET || process.env.TARGET_SECRET || '';
|
|
18
|
+
|
|
19
|
+
const envPath = args.env || '.env.json';
|
|
20
|
+
const envFile = path.resolve(__root, envPath);
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(envFile)) {
|
|
23
|
+
console.error('No env file found.');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
9
26
|
|
|
10
|
-
// Here we fetch the target env from cli args
|
|
11
27
|
const DEFAULT_ENV = 'dev';
|
|
12
28
|
const targets = {
|
|
13
29
|
"development": "dev",
|
|
@@ -18,187 +34,237 @@ const targets = {
|
|
|
18
34
|
"prod": "production",
|
|
19
35
|
"production": "production",
|
|
20
36
|
}
|
|
21
|
-
const target = targets[args.target] || DEFAULT_ENV;
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
async function showCommand() {
|
|
39
|
+
if (!secret) {
|
|
40
|
+
console.log('Secret not provided. Use --secret flag or WRENV_SECRET env variable.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const target = targets[args.target || process.env.WRENV_TARGET || process.env.TARGET_ENV] || DEFAULT_ENV;
|
|
44
|
+
|
|
45
|
+
const envContent = fs.readFileSync(envFile, 'utf8');
|
|
46
|
+
const env = JSON.parse(envContent);
|
|
47
|
+
|
|
48
|
+
const rootPkgPath = path.resolve(__root, 'package.json');
|
|
49
|
+
const rootPkgContent = fs.readFileSync(rootPkgPath, 'utf8');
|
|
50
|
+
const rootPkg = JSON.parse(rootPkgContent);
|
|
51
|
+
const scope = rootPkg.name.split('/')[0];
|
|
52
|
+
|
|
53
|
+
const [_, ...foundPackages] = await findMonorepoPackages(__root, scope);
|
|
54
|
+
const deps = [
|
|
55
|
+
...foundPackages.map((pkg) => ({
|
|
56
|
+
name: pkg.name.replace(`${scope}/`, ''),
|
|
57
|
+
dir: pkg.dir,
|
|
58
|
+
})),
|
|
59
|
+
{
|
|
60
|
+
name: 'root',
|
|
61
|
+
dir: __root,
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
const depsName = deps.map((dep) => dep.name);
|
|
65
|
+
|
|
66
|
+
const allowedEnvs = env && env.envs && Array.isArray(env.envs) ? env.envs : [DEFAULT_ENV];
|
|
67
|
+
const targetEnv = allowedEnvs.find((env) => env === target);
|
|
68
|
+
if (!targetEnv) {
|
|
69
|
+
console.error(`Target env "${target}" is not allowed.`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const allEnvs = buildEnv(secret, targetEnv, env, depsName);
|
|
74
|
+
|
|
75
|
+
for await (const dep of deps) {
|
|
76
|
+
const pkgEnv = Object.entries(allEnvs[dep.name] || {})
|
|
77
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
78
|
+
.join('\n')
|
|
79
|
+
console.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~~');
|
|
80
|
+
console.log(`=== ENV for ${dep.name} in ${dep.dir} with target ${targetEnv}`);
|
|
81
|
+
console.log(pkgEnv);
|
|
82
|
+
}
|
|
28
83
|
|
|
29
|
-
if (!envFile) {
|
|
30
|
-
console.error('No env file found.');
|
|
31
|
-
process.exit(1);
|
|
32
84
|
}
|
|
33
85
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
name
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
async function writeCommand() {
|
|
87
|
+
if (!secret) {
|
|
88
|
+
console.log('Secret not provided. Use --secret flag or WRENV_SECRET env variable.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const target = targets[args.target || process.env.WRENV_TARGET || process.env.TARGET_ENV] || DEFAULT_ENV;
|
|
92
|
+
|
|
93
|
+
const envContent = fs.readFileSync(envFile, 'utf8');
|
|
94
|
+
const env = JSON.parse(envContent);
|
|
95
|
+
|
|
96
|
+
const rootPkgPath = path.resolve(__root, 'package.json');
|
|
97
|
+
const rootPkgContent = fs.readFileSync(rootPkgPath, 'utf8');
|
|
98
|
+
const rootPkg = JSON.parse(rootPkgContent);
|
|
99
|
+
const scope = rootPkg.name.split('/')[0];
|
|
100
|
+
|
|
101
|
+
const [_, ...foundPackages] = await findMonorepoPackages(__root, scope);
|
|
102
|
+
const deps = [
|
|
103
|
+
...foundPackages.map((pkg) => ({
|
|
104
|
+
name: pkg.name.replace(`${scope}/`, ''),
|
|
105
|
+
dir: pkg.dir,
|
|
106
|
+
})),
|
|
107
|
+
{
|
|
108
|
+
name: 'root',
|
|
109
|
+
dir: __root,
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
const depsName = deps.map((dep) => dep.name);
|
|
113
|
+
|
|
114
|
+
const allowedEnvs = env && env.envs && Array.isArray(env.envs) ? env.envs : [DEFAULT_ENV];
|
|
115
|
+
const targetEnv = allowedEnvs.find((env) => env === target);
|
|
116
|
+
if (!targetEnv) {
|
|
117
|
+
console.error(`Target env "${target}" is not allowed.`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const allEnvs = buildEnv(secret, targetEnv, env, depsName);
|
|
122
|
+
|
|
123
|
+
for await (const dep of deps) {
|
|
124
|
+
const pkgEnv = Object.entries(allEnvs[dep.name] || {})
|
|
125
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
126
|
+
.join('\n')
|
|
127
|
+
console.log(`Writing env file for ${dep.name} in ${dep.dir} with target ${targetEnv}`);
|
|
128
|
+
await fs.writeFile(path.join(dep.dir, '.env'), pkgEnv);
|
|
129
|
+
}
|
|
72
130
|
}
|
|
73
131
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
|
|
132
|
+
async function encryptCommand() {
|
|
133
|
+
const envContent = fs.readFileSync(envFile, 'utf8');
|
|
134
|
+
const env = JSON.parse(envContent);
|
|
135
|
+
|
|
136
|
+
if (!secret) {
|
|
137
|
+
console.error('Cannot encrypt env file: secret not provided. Doing nothing');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (env.variables) {
|
|
142
|
+
for (const [varName, varValue] of Object.entries(env.variables)) {
|
|
143
|
+
for (const [envKey, envVal] of Object.entries(varValue)) {
|
|
144
|
+
if (typeof envVal === 'string' && !isEncrypted(envVal)) {
|
|
145
|
+
env.variables[varName][envKey] = encrypt(envVal, secret);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (env.overrides) {
|
|
152
|
+
for (const [pkgName, pkgOverrides] of Object.entries(env.overrides)) {
|
|
153
|
+
for (const [varName, varValue] of Object.entries(pkgOverrides)) {
|
|
154
|
+
for (const [envKey, envVal] of Object.entries(varValue)) {
|
|
155
|
+
if (typeof envVal === 'string' && !isEncrypted(envVal)) {
|
|
156
|
+
env.overrides[pkgName][varName][envKey] = encrypt(envVal, secret);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await fs.writeFile(envFile, JSON.stringify(env, null, 2));
|
|
87
164
|
}
|
|
88
165
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// ========================================
|
|
92
|
-
|
|
93
|
-
function buildEnv(target, env, names) {
|
|
94
|
-
const variablesForAllTargets = env.variables;
|
|
95
|
-
const overrides = env.overrides || {};
|
|
96
|
-
const sections = env.sections || {};
|
|
97
|
-
|
|
98
|
-
// For each variables, extract the value for the target env
|
|
99
|
-
const variables = Object.entries(variablesForAllTargets).reduce((acc, entry) => {
|
|
100
|
-
const [key, value] = entry;
|
|
101
|
-
return {
|
|
102
|
-
...acc,
|
|
103
|
-
[key]: value[target] || value['@@'] || ''
|
|
104
|
-
};
|
|
105
|
-
}, {});
|
|
106
|
-
|
|
107
|
-
// For each package, get all variables
|
|
108
|
-
// compute the overrides and merge them
|
|
109
|
-
// allVars is an object with the package name as key
|
|
110
|
-
// and the variables as value like so
|
|
111
|
-
// => { [name]: { [variable]: value } }
|
|
112
|
-
const allVars = names.reduce((acc, name) => {
|
|
113
|
-
acc[name] = {
|
|
114
|
-
...variables,
|
|
115
|
-
...parseOverrides(target, overrides[name], variables)
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
return acc;
|
|
119
|
-
}, {});
|
|
120
|
-
|
|
121
|
-
// For each package, get all dependencies of variables
|
|
122
|
-
// For instance and for clarity, backoffice can include all vars
|
|
123
|
-
// from the api, and the api can include all vars from the core
|
|
124
|
-
const allDeps = names.reduce((acc, name) => ({
|
|
125
|
-
...acc,
|
|
126
|
-
[name]: parseSectionDeps(name, sections).reverse()
|
|
127
|
-
}), {})
|
|
128
|
-
|
|
129
|
-
// For each package, get all variable names from the dependencies
|
|
130
|
-
// and from the package itself and merge them
|
|
131
|
-
const allSections = Object.entries(allDeps).reduce((acc, entry) => {
|
|
132
|
-
const [key, deps] = entry;
|
|
133
|
-
|
|
134
|
-
// For all deps, get the variable names
|
|
135
|
-
const depVars = deps.filter(Boolean).reduce((_acc, dep) => {
|
|
136
|
-
const vars = parseSectionVars(dep, sections)
|
|
137
|
-
return [..._acc, ...vars];
|
|
138
|
-
}, []);
|
|
139
|
-
|
|
140
|
-
// For the current section, get the variable names
|
|
141
|
-
const nameVars = parseSectionVars(key, sections)
|
|
142
|
-
|
|
143
|
-
// Merge all variables names, and for each variable name,
|
|
144
|
-
// get the value from the allVars object
|
|
145
|
-
return {
|
|
146
|
-
...acc,
|
|
147
|
-
[key]: [...depVars, ...nameVars].reduce((_acc, variable) => {
|
|
148
|
-
if (!allVars[key] || allVars[key][variable] === null) return _acc;
|
|
149
|
-
|
|
150
|
-
return { ..._acc, [variable]: allVars[key][variable] };
|
|
151
|
-
}, {})
|
|
152
|
-
};
|
|
153
|
-
}, {});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return allSections;
|
|
157
|
-
}
|
|
166
|
+
async function besafeCommand() {
|
|
167
|
+
const relativeEnvPath = path.relative(__root, envFile);
|
|
158
168
|
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
try {
|
|
170
|
+
const { stdout: stagedDiff } = await execa('git', ['diff', '--cached', '--name-only', '--', relativeEnvPath], { cwd: __root, reject: false });
|
|
171
|
+
const { stdout: unstagedDiff } = await execa('git', ['diff', '--name-only', '--', relativeEnvPath], { cwd: __root, reject: false });
|
|
172
|
+
const { stdout: untracked } = await execa('git', ['ls-files', '--others', '--exclude-standard', '--', relativeEnvPath], { cwd: __root, reject: false });
|
|
161
173
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
174
|
+
if (!stagedDiff?.trim() && !unstagedDiff?.trim() && !untracked?.trim()) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
165
180
|
|
|
166
|
-
|
|
181
|
+
await encrypt();
|
|
167
182
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
183
|
+
if (!secret) {
|
|
184
|
+
console.error('Warning: potential security risk: secret not provided. Variables WERE NOT be encrypted.');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await execa('git', ['add', relativeEnvPath], { cwd: __root });
|
|
173
188
|
}
|
|
174
189
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
190
|
+
async function addCommand() {
|
|
191
|
+
const varName = args._[1];
|
|
192
|
+
if (!varName) {
|
|
193
|
+
console.error('Variable name is required.');
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!secret) {
|
|
198
|
+
console.error('Warning: potential security risk: secret not provided. Variable WILL NOT be encrypted.');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const envContent = fs.readFileSync(envFile, 'utf8');
|
|
202
|
+
const env = JSON.parse(envContent);
|
|
203
|
+
|
|
204
|
+
if (!env.variables) {
|
|
205
|
+
env.variables = {};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!env.variables[varName]) {
|
|
209
|
+
env.variables[varName] = {};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const arg of args._.slice(2)) {
|
|
213
|
+
if (!arg.startsWith('+')) continue;
|
|
214
|
+
const [key, ...valueParts] = arg.slice(1).split('=');
|
|
215
|
+
const value = valueParts.join('=');
|
|
216
|
+
const envKey = key === 'fallback' ? '@@' : key;
|
|
217
|
+
env.variables[varName][envKey] = secret ? encrypt(value, secret) : value;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
await fs.writeFile(envFile, JSON.stringify(env, null, 2));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function helpCommand() {
|
|
224
|
+
console.log('Usage: wrenv [command] [options]\n');
|
|
225
|
+
console.log('Commands:');
|
|
226
|
+
console.log(' write Write .env files for all packages (default)');
|
|
227
|
+
console.log(' show Show environment variables without writing files');
|
|
228
|
+
console.log(' encrypt Encrypt all unencrypted variables in .env.json');
|
|
229
|
+
console.log(' besafe Encrypt and stage .env.json if it has changes');
|
|
230
|
+
console.log(' add <var> Add a new variable with environment-specific values');
|
|
231
|
+
console.log(' help Show this help message\n');
|
|
232
|
+
console.log('Options:');
|
|
233
|
+
console.log(' --secret <path|stdin> Path to secret file or "stdin" to read from stdin');
|
|
234
|
+
console.log(' --target <env> Target environment (dev, staging, prod, etc.)');
|
|
235
|
+
console.log(' --env <path> Path to env file (default: .env.json)\n');
|
|
236
|
+
console.log('Environment Variables:');
|
|
237
|
+
console.log(' WRENV_SECRET Secret for encryption/decryption');
|
|
238
|
+
console.log(' WRENV_TARGET Target environment');
|
|
239
|
+
console.log(' TARGET_SECRET Alias for WRENV_SECRET');
|
|
240
|
+
console.log(' TARGET_ENV Alias for WRENV_TARGET\n');
|
|
241
|
+
console.log('Examples:');
|
|
242
|
+
console.log(' wrenv --secret ~/.big-secret write --target dev');
|
|
243
|
+
console.log(' wrenv --secret stdin write < ~/.big-secret');
|
|
244
|
+
console.log(' wrenv show --target prod');
|
|
245
|
+
console.log(' wrenv add NEW_VAR +fallback=value +dev=dev_value +production=prod_value');
|
|
197
246
|
}
|
|
198
247
|
|
|
199
|
-
|
|
200
|
-
|
|
248
|
+
const commands = {
|
|
249
|
+
write: writeCommand,
|
|
250
|
+
show: showCommand,
|
|
251
|
+
besafe: besafeCommand,
|
|
252
|
+
encrypt: encryptCommand,
|
|
253
|
+
add: addCommand,
|
|
254
|
+
help: helpCommand,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const commandFunction = commands[command];
|
|
258
|
+
|
|
259
|
+
if (!commandFunction) {
|
|
260
|
+
console.error(`Unknown command: ${command}\n`);
|
|
261
|
+
helpCommand();
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
201
264
|
|
|
202
|
-
|
|
203
|
-
|
|
265
|
+
try {
|
|
266
|
+
await commandFunction();
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error(error.message);
|
|
269
|
+
process.exit(1);
|
|
204
270
|
}
|