nobalmako 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 +112 -0
- package/components.json +22 -0
- package/dist/nobalmako.js +272 -0
- package/drizzle/0000_pink_spiral.sql +126 -0
- package/drizzle/meta/0000_snapshot.json +1027 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +7 -0
- package/package.json +80 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/index.ts +118 -0
- package/src/app/api/api-keys/[id]/route.ts +147 -0
- package/src/app/api/api-keys/route.ts +151 -0
- package/src/app/api/audit-logs/route.ts +84 -0
- package/src/app/api/auth/forgot-password/route.ts +47 -0
- package/src/app/api/auth/login/route.ts +99 -0
- package/src/app/api/auth/logout/route.ts +15 -0
- package/src/app/api/auth/me/route.ts +23 -0
- package/src/app/api/auth/mfa/setup/route.ts +33 -0
- package/src/app/api/auth/mfa/verify/route.ts +45 -0
- package/src/app/api/auth/register/route.ts +140 -0
- package/src/app/api/auth/reset-password/route.ts +52 -0
- package/src/app/api/auth/update/route.ts +71 -0
- package/src/app/api/auth/verify/route.ts +39 -0
- package/src/app/api/environments/route.ts +227 -0
- package/src/app/api/team-members/route.ts +385 -0
- package/src/app/api/teams/route.ts +217 -0
- package/src/app/api/variable-history/route.ts +218 -0
- package/src/app/api/variables/route.ts +476 -0
- package/src/app/api/webhooks/route.ts +77 -0
- package/src/app/api-keys/APIKeysClient.tsx +316 -0
- package/src/app/api-keys/page.tsx +10 -0
- package/src/app/api-reference/page.tsx +324 -0
- package/src/app/audit-log/AuditLogClient.tsx +229 -0
- package/src/app/audit-log/page.tsx +10 -0
- package/src/app/auth/forgot-password/page.tsx +121 -0
- package/src/app/auth/login/LoginForm.tsx +145 -0
- package/src/app/auth/login/page.tsx +11 -0
- package/src/app/auth/register/RegisterForm.tsx +156 -0
- package/src/app/auth/register/page.tsx +16 -0
- package/src/app/auth/reset-password/page.tsx +160 -0
- package/src/app/dashboard/DashboardClient.tsx +219 -0
- package/src/app/dashboard/page.tsx +11 -0
- package/src/app/docs/page.tsx +251 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +231 -0
- package/src/app/profile/ProfileClient.tsx +230 -0
- package/src/app/profile/page.tsx +10 -0
- package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
- package/src/app/project/[id]/page.tsx +17 -0
- package/src/bin/nobalmako.ts +341 -0
- package/src/components/ApiKeysManager.tsx +529 -0
- package/src/components/AppLayout.tsx +193 -0
- package/src/components/BulkActions.tsx +138 -0
- package/src/components/CreateEnvironmentDialog.tsx +207 -0
- package/src/components/CreateTeamDialog.tsx +174 -0
- package/src/components/CreateVariableDialog.tsx +311 -0
- package/src/components/DeleteEnvironmentDialog.tsx +104 -0
- package/src/components/DeleteTeamDialog.tsx +112 -0
- package/src/components/DeleteVariableDialog.tsx +103 -0
- package/src/components/EditEnvironmentDialog.tsx +202 -0
- package/src/components/EditMemberDialog.tsx +143 -0
- package/src/components/EditTeamDialog.tsx +178 -0
- package/src/components/EditVariableDialog.tsx +231 -0
- package/src/components/ImportVariablesDialog.tsx +347 -0
- package/src/components/InviteMemberDialog.tsx +191 -0
- package/src/components/LeaveProjectDialog.tsx +111 -0
- package/src/components/MFASettings.tsx +136 -0
- package/src/components/ProjectDiff.tsx +123 -0
- package/src/components/Providers.tsx +24 -0
- package/src/components/RemoveMemberDialog.tsx +112 -0
- package/src/components/SearchDialog.tsx +276 -0
- package/src/components/SecurityOverview.tsx +92 -0
- package/src/components/TeamMembersManager.tsx +103 -0
- package/src/components/VariableHistoryDialog.tsx +265 -0
- package/src/components/WebhooksManager.tsx +169 -0
- package/src/components/ui/alert-dialog.tsx +160 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/hooks/use-api-keys.ts +95 -0
- package/src/hooks/use-audit-logs.ts +58 -0
- package/src/hooks/use-auth.tsx +121 -0
- package/src/hooks/use-environments.ts +33 -0
- package/src/hooks/use-project-permissions.ts +49 -0
- package/src/hooks/use-team-members.ts +30 -0
- package/src/hooks/use-teams.ts +33 -0
- package/src/hooks/use-variables.ts +38 -0
- package/src/lib/audit.ts +36 -0
- package/src/lib/auth.ts +108 -0
- package/src/lib/crypto.ts +39 -0
- package/src/lib/db.ts +15 -0
- package/src/lib/dynamic-providers.ts +19 -0
- package/src/lib/email.ts +110 -0
- package/src/lib/mail.ts +51 -0
- package/src/lib/permissions.ts +51 -0
- package/src/lib/schema.ts +240 -0
- package/src/lib/seed.ts +107 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/webhooks.ts +42 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import dotenv from 'dotenv';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
import { prompt } from 'enquirer';
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
const CONFIG_FILE = path.join(os.homedir(), '.nobalmako.json');
|
|
13
|
+
const PROJECT_CONFIG = path.join(process.cwd(), 'nobalmako.json');
|
|
14
|
+
|
|
15
|
+
function saveConfig(config: any) {
|
|
16
|
+
const current = loadConfig();
|
|
17
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...config }, null, 2));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadConfig() {
|
|
21
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
22
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
23
|
+
}
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadProjectConfig() {
|
|
28
|
+
if (fs.existsSync(PROJECT_CONFIG)) {
|
|
29
|
+
return JSON.parse(fs.readFileSync(PROJECT_CONFIG, 'utf-8'));
|
|
30
|
+
}
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getToken(optionsToken?: string) {
|
|
35
|
+
const config = loadConfig();
|
|
36
|
+
return optionsToken || process.env.NOBALMAKO_TOKEN || config.token;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.name('nobalmako')
|
|
41
|
+
.description('Securing your environment variables')
|
|
42
|
+
.version('1.0.0');
|
|
43
|
+
|
|
44
|
+
program
|
|
45
|
+
.command('login')
|
|
46
|
+
.description('Login to Nobalmako')
|
|
47
|
+
.option('--email <email>', 'User email')
|
|
48
|
+
.option('--password <password>', 'User password')
|
|
49
|
+
.option('--api-url <url>', 'Base API URL', 'http://localhost:3000/api')
|
|
50
|
+
.action(async (options) => {
|
|
51
|
+
let { email, password } = options;
|
|
52
|
+
|
|
53
|
+
if (!email) {
|
|
54
|
+
const response = await prompt({
|
|
55
|
+
type: 'input',
|
|
56
|
+
name: 'email',
|
|
57
|
+
message: 'Enter your email:'
|
|
58
|
+
}) as any;
|
|
59
|
+
email = response.email;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!password) {
|
|
63
|
+
const response = await prompt({
|
|
64
|
+
type: 'password',
|
|
65
|
+
name: 'password',
|
|
66
|
+
message: 'Enter your password:'
|
|
67
|
+
}) as any;
|
|
68
|
+
password = response.password;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(`${options.apiUrl}/auth/login`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify({ email, password })
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error('Login failed. Please check your credentials.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
saveConfig({ token: data.token, email: data.user.email });
|
|
84
|
+
|
|
85
|
+
console.log(`\x1b[32mSuccessfully logged in as ${data.user.email}!\x1b[0m`);
|
|
86
|
+
} catch (error: any) {
|
|
87
|
+
console.error(`\x1b[31mError:\x1b[0m ${error.message}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
program
|
|
93
|
+
.command('logout')
|
|
94
|
+
.description('Clear local credentials')
|
|
95
|
+
.action(() => {
|
|
96
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
97
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
98
|
+
console.log('\x1b[32mSuccessfully logged out.\x1b[0m');
|
|
99
|
+
} else {
|
|
100
|
+
console.log('You are not logged in.');
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
program
|
|
105
|
+
.command('init')
|
|
106
|
+
.description('Initialize a local project configuration')
|
|
107
|
+
.option('-p, --project <project>', 'Project name')
|
|
108
|
+
.option('-e, --env <environment>', 'Default environment name')
|
|
109
|
+
.action(async (options) => {
|
|
110
|
+
let { project, env } = options;
|
|
111
|
+
|
|
112
|
+
if (!project) {
|
|
113
|
+
const response = await prompt({
|
|
114
|
+
type: 'input',
|
|
115
|
+
name: 'project',
|
|
116
|
+
message: 'Project name (slug):'
|
|
117
|
+
}) as any;
|
|
118
|
+
project = response.project;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!env) {
|
|
122
|
+
const response = await prompt({
|
|
123
|
+
type: 'input',
|
|
124
|
+
name: 'env',
|
|
125
|
+
message: 'Default environment (e.g. production):'
|
|
126
|
+
}) as any;
|
|
127
|
+
env = response.env;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fs.writeFileSync(PROJECT_CONFIG, JSON.stringify({ project, environment: env }, null, 2));
|
|
131
|
+
console.log(`\x1b[32mCreated nobalmako.json with defaults.\x1b[0m`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
program
|
|
135
|
+
.command('pull')
|
|
136
|
+
.description('Pull environment variables from Nobalmako and save to .env')
|
|
137
|
+
.option('-p, --project <project>', 'Project name')
|
|
138
|
+
.option('-e, --env <environment>', 'Environment name (e.g. production, staging)')
|
|
139
|
+
.option('-f, --file <filename>', 'Output filename')
|
|
140
|
+
.option('--api-url <url>', 'Base API URL', 'http://localhost:3000/api')
|
|
141
|
+
.option('-t, --token <token>', 'API Token (overrides login)')
|
|
142
|
+
.action(async (options) => {
|
|
143
|
+
const token = getToken(options.token);
|
|
144
|
+
const pConfig = loadProjectConfig();
|
|
145
|
+
|
|
146
|
+
const project = options.project || pConfig.project;
|
|
147
|
+
const env = options.env || pConfig.environment;
|
|
148
|
+
const file = options.file || pConfig.file || '.env';
|
|
149
|
+
|
|
150
|
+
if (!token) {
|
|
151
|
+
console.error('\x1b[31mError: Not logged in.\x1b[0m Run \x1b[33mnobalmako login\x1b[0m or set \x1b[33mNOBALMAKO_TOKEN\x1b[0m.');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!project || !env) {
|
|
156
|
+
console.error('\x1b[31mError: Project and Environment are required.\x1b[0m Use \x1b[33mnobalmako init\x1b[0m or provide flags.');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`\x1b[34m[Nobalmako]\x1b[0m Fetching secrets for project \x1b[36m${project}\x1b[0m (\x1b[35m${env}\x1b[0m)...`);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const response = await fetch(`${options.apiUrl}/variables?team=${encodeURIComponent(project)}&environment=${encodeURIComponent(env)}`, {
|
|
164
|
+
headers: {
|
|
165
|
+
'Authorization': `Bearer ${token}`
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
const errorData = await response.json();
|
|
171
|
+
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const data = await response.json();
|
|
175
|
+
const variables = data.variables || [];
|
|
176
|
+
|
|
177
|
+
if (variables.length === 0) {
|
|
178
|
+
console.log('\x1b[33mNo variables found for this project and environment.\x1b[0m');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let envContent = `# Generated by Nobalmako on ${new Date().toISOString()}\n`;
|
|
183
|
+
variables.forEach((v: any) => {
|
|
184
|
+
if (v.description) {
|
|
185
|
+
envContent += `# ${v.description}\n`;
|
|
186
|
+
}
|
|
187
|
+
envContent += `${v.key}=${v.value}\n`;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const outputPath = path.resolve(process.cwd(), file);
|
|
191
|
+
fs.writeFileSync(outputPath, envContent);
|
|
192
|
+
|
|
193
|
+
console.log(`\x1b[32mSuccess!\x1b[0m Pulled ${variables.length} variables into \x1b[33m${file}\x1b[0m`);
|
|
194
|
+
} catch (error: any) {
|
|
195
|
+
console.error(`\x1b[31mPull failed:\x1b[0m ${error.message}`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
program
|
|
201
|
+
.command('push')
|
|
202
|
+
.description('Push environment variables from a .env file to Nobalmako')
|
|
203
|
+
.option('-p, --project <project>', 'Project name')
|
|
204
|
+
.option('-e, --env <environment>', 'Environment name (e.g. production, staging)')
|
|
205
|
+
.option('-f, --file <filename>', 'Input filename')
|
|
206
|
+
.option('-s, --secret', 'Mark all variables as secret', false)
|
|
207
|
+
.option('--api-url <url>', 'Base API URL', 'http://localhost:3000/api')
|
|
208
|
+
.option('-t, --token <token>', 'API Token (overrides login)')
|
|
209
|
+
.action(async (options) => {
|
|
210
|
+
const token = getToken(options.token);
|
|
211
|
+
const pConfig = loadProjectConfig();
|
|
212
|
+
|
|
213
|
+
const project = options.project || pConfig.project;
|
|
214
|
+
const env = options.env || pConfig.environment;
|
|
215
|
+
const file = options.file || pConfig.file || '.env';
|
|
216
|
+
|
|
217
|
+
if (!token) {
|
|
218
|
+
console.error('\x1b[31mError: Not logged in.\x1b[0m Run \x1b[33mnobalmako login\x1b[0m or set \x1b[33mNOBALMAKO_TOKEN\x1b[0m.');
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!project || !env) {
|
|
223
|
+
console.error('\x1b[31mError: Project and Environment are required.\x1b[0m Use \x1b[33mnobalmako init\x1b[0m or provide flags.');
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const inputPath = path.resolve(process.cwd(), file);
|
|
228
|
+
if (!fs.existsSync(inputPath)) {
|
|
229
|
+
console.error(`\x1b[31mError: File not found: ${file}\x1b[0m`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const envContent = fs.readFileSync(inputPath, 'utf-8');
|
|
234
|
+
const envVars = dotenv.parse(envContent);
|
|
235
|
+
const keys = Object.keys(envVars);
|
|
236
|
+
|
|
237
|
+
console.log(`\x1b[34m[Nobalmako]\x1b[0m Pushing ${keys.length} variables to \x1b[36m${project}\x1b[0m (\x1b[35m${env}\x1b[0m)...`);
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
// 1. Get Project ID and Environment ID
|
|
241
|
+
const envsResponse = await fetch(`${options.apiUrl}/environments`, {
|
|
242
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
243
|
+
});
|
|
244
|
+
if (!envsResponse.ok) throw new Error('Failed to fetch environments');
|
|
245
|
+
const envsData = await envsResponse.json();
|
|
246
|
+
const targetEnv = envsData.environments.find((e: any) =>
|
|
247
|
+
e.teamName === project && e.name === env
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
if (!targetEnv) {
|
|
251
|
+
throw new Error(`Could not find environment "${env}" for project "${project}".`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 2. Push each variable
|
|
255
|
+
let successCount = 0;
|
|
256
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
257
|
+
const response = await fetch(`${options.apiUrl}/variables`, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: {
|
|
260
|
+
'Authorization': `Bearer ${token}`,
|
|
261
|
+
'Content-Type': 'application/json'
|
|
262
|
+
},
|
|
263
|
+
body: JSON.stringify({
|
|
264
|
+
key,
|
|
265
|
+
value,
|
|
266
|
+
teamId: targetEnv.teamId,
|
|
267
|
+
environmentId: targetEnv.id,
|
|
268
|
+
isSecret: options.secret
|
|
269
|
+
})
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (response.ok) {
|
|
273
|
+
successCount++;
|
|
274
|
+
process.stdout.write(`\rProgress: ${successCount}/${keys.length}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(`\n\x1b[32mSuccess!\x1b[0m Pushed ${successCount} variables to \x1b[33m${project}\x1b[0m`);
|
|
279
|
+
} catch (error: any) {
|
|
280
|
+
console.error(`\x1b[31mPush failed:\x1b[0m ${error.message}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
program
|
|
286
|
+
.command('run')
|
|
287
|
+
.description('Run a command with environment variables injected')
|
|
288
|
+
.argument('<command...>', 'The command to execute')
|
|
289
|
+
.option('-p, --project <project>', 'Project name')
|
|
290
|
+
.option('-e, --env <environment>', 'Environment name')
|
|
291
|
+
.option('--api-url <url>', 'Base API URL', 'http://localhost:3000/api')
|
|
292
|
+
.option('-t, --token <token>', 'API Token')
|
|
293
|
+
.action(async (commandArgs, options) => {
|
|
294
|
+
const token = getToken(options.token);
|
|
295
|
+
const pConfig = loadProjectConfig();
|
|
296
|
+
|
|
297
|
+
const project = options.project || pConfig.project;
|
|
298
|
+
const env = options.env || pConfig.environment;
|
|
299
|
+
|
|
300
|
+
if (!token) {
|
|
301
|
+
console.error('\x1b[31mError: Not logged in.\x1b[0m');
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!project || !env) {
|
|
306
|
+
console.error('\x1b[31mError: Project and Environment are required.\x1b[0m');
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const response = await fetch(`${options.apiUrl}/variables?team=${encodeURIComponent(project)}&environment=${encodeURIComponent(env)}`, {
|
|
312
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (!response.ok) throw new Error('Failed to fetch variables');
|
|
316
|
+
const data = await response.json();
|
|
317
|
+
const variables = data.variables || [];
|
|
318
|
+
|
|
319
|
+
// Create new env object
|
|
320
|
+
const newEnv = { ...process.env };
|
|
321
|
+
variables.forEach((v: any) => {
|
|
322
|
+
newEnv[v.key] = v.value;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const [cmd, ...args] = commandArgs;
|
|
326
|
+
const child = spawn(cmd, args, {
|
|
327
|
+
stdio: 'inherit',
|
|
328
|
+
env: newEnv,
|
|
329
|
+
shell: true
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
child.on('exit', (code) => {
|
|
333
|
+
process.exit(code || 0);
|
|
334
|
+
});
|
|
335
|
+
} catch (error: any) {
|
|
336
|
+
console.error(`\x1b[31mRun failed:\x1b[0m ${error.message}`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
program.parse(process.argv);
|