hackerrun 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/.claude/settings.local.json +22 -0
- package/.env.example +9 -0
- package/CLAUDE.md +532 -0
- package/README.md +94 -0
- package/dist/index.js +2813 -0
- package/package.json +38 -0
- package/src/commands/app.ts +394 -0
- package/src/commands/builds.ts +314 -0
- package/src/commands/config.ts +129 -0
- package/src/commands/connect.ts +197 -0
- package/src/commands/deploy.ts +227 -0
- package/src/commands/env.ts +174 -0
- package/src/commands/login.ts +120 -0
- package/src/commands/logs.ts +97 -0
- package/src/index.ts +43 -0
- package/src/lib/app-config.ts +95 -0
- package/src/lib/cluster.ts +428 -0
- package/src/lib/config.ts +137 -0
- package/src/lib/platform-auth.ts +20 -0
- package/src/lib/platform-client.ts +637 -0
- package/src/lib/platform.ts +87 -0
- package/src/lib/ssh-cert.ts +264 -0
- package/src/lib/uncloud-runner.ts +342 -0
- package/src/lib/uncloud.ts +149 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +17 -0
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hackerrun",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool to create and manage VMs using Ubicloud API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hackerrun": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsup --watch",
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"start": "node dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ubicloud",
|
|
16
|
+
"vm",
|
|
17
|
+
"cli",
|
|
18
|
+
"cloud"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@inquirer/prompts": "^7.0.0",
|
|
24
|
+
"chalk": "^5.3.0",
|
|
25
|
+
"commander": "^12.0.0",
|
|
26
|
+
"dotenv": "^16.4.7",
|
|
27
|
+
"ora": "^8.1.1",
|
|
28
|
+
"yaml": "^2.8.2"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.10.2",
|
|
32
|
+
"tsup": "^8.3.5",
|
|
33
|
+
"typescript": "^5.7.2"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { getPlatformToken } from '../lib/platform-auth.js';
|
|
9
|
+
import { PlatformClient } from '../lib/platform-client.js';
|
|
10
|
+
import { linkApp, readAppConfig } from '../lib/app-config.js';
|
|
11
|
+
import { UncloudRunner } from '../lib/uncloud-runner.js';
|
|
12
|
+
import YAML from 'yaml';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Remove an uncloud context from the config file
|
|
16
|
+
*/
|
|
17
|
+
function removeUncloudContext(contextName: string): boolean {
|
|
18
|
+
const configPath = join(homedir(), '.config', 'uncloud', 'config.yaml');
|
|
19
|
+
|
|
20
|
+
if (!existsSync(configPath)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
26
|
+
const config = YAML.parse(content);
|
|
27
|
+
|
|
28
|
+
if (!config.contexts || !config.contexts[contextName]) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Remove the context
|
|
33
|
+
delete config.contexts[contextName];
|
|
34
|
+
|
|
35
|
+
// If current context was the deleted one, switch to another context
|
|
36
|
+
if (config.current_context === contextName) {
|
|
37
|
+
const remainingContexts = Object.keys(config.contexts);
|
|
38
|
+
config.current_context = remainingContexts.length > 0 ? remainingContexts[0] : '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
writeFileSync(configPath, YAML.stringify(config));
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createAppCommands() {
|
|
49
|
+
// Apps list command
|
|
50
|
+
const appsCmd = new Command('apps');
|
|
51
|
+
appsCmd
|
|
52
|
+
.description('List all your apps and their infrastructure')
|
|
53
|
+
.action(async () => {
|
|
54
|
+
try {
|
|
55
|
+
// Fetch apps from backend (single source of truth)
|
|
56
|
+
const platformToken = getPlatformToken();
|
|
57
|
+
const platformClient = new PlatformClient(platformToken);
|
|
58
|
+
const apps = await platformClient.listApps();
|
|
59
|
+
|
|
60
|
+
if (apps.length === 0) {
|
|
61
|
+
console.log(chalk.yellow('\nNo apps deployed yet.\n'));
|
|
62
|
+
console.log(`Run ${chalk.bold('hackerrun deploy')} to deploy your first app!\n`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(chalk.cyan('\n Your Apps:\n'));
|
|
67
|
+
|
|
68
|
+
apps.forEach(app => {
|
|
69
|
+
console.log(chalk.bold(` ${app.appName}`));
|
|
70
|
+
console.log(` Domain: ${app.domainName}.hackerrun.app`);
|
|
71
|
+
console.log(` URL: https://${app.domainName}.hackerrun.app`);
|
|
72
|
+
console.log(` Location: ${app.location}`);
|
|
73
|
+
console.log(` Nodes: ${app.nodes.length}`);
|
|
74
|
+
console.log(` Created: ${new Date(app.createdAt).toLocaleString()}`);
|
|
75
|
+
if (app.lastDeployedAt) {
|
|
76
|
+
console.log(` Last Deploy: ${new Date(app.lastDeployedAt).toLocaleString()}`);
|
|
77
|
+
}
|
|
78
|
+
const primaryNode = app.nodes.find(n => n.isPrimary);
|
|
79
|
+
console.log(` Primary Node: ${primaryNode?.ipv6 || primaryNode?.ipv4 || 'N/A'}`);
|
|
80
|
+
console.log();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
console.log(chalk.green(`Total: ${apps.length} app(s)\n`));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Nodes list command
|
|
91
|
+
const nodesCmd = new Command('nodes');
|
|
92
|
+
nodesCmd
|
|
93
|
+
.description('List all VMs in an app\'s cluster')
|
|
94
|
+
.argument('<app>', 'App name')
|
|
95
|
+
.action(async (appName: string) => {
|
|
96
|
+
try {
|
|
97
|
+
const platformToken = getPlatformToken();
|
|
98
|
+
const platformClient = new PlatformClient(platformToken);
|
|
99
|
+
const app = await platformClient.getApp(appName);
|
|
100
|
+
|
|
101
|
+
if (!app) {
|
|
102
|
+
console.log(chalk.red(`\nApp '${appName}' not found\n`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(chalk.cyan(`\nNodes in '${appName}':\n`));
|
|
107
|
+
|
|
108
|
+
app.nodes.forEach((node, index) => {
|
|
109
|
+
const primaryLabel = node.isPrimary ? chalk.yellow(' (primary)') : '';
|
|
110
|
+
console.log(` ${index + 1}. ${chalk.bold(node.name)}${primaryLabel}`);
|
|
111
|
+
console.log(` IPv6: ${node.ipv6 || 'pending'}`);
|
|
112
|
+
if (node.ipv4) {
|
|
113
|
+
console.log(` IPv4: ${node.ipv4}`);
|
|
114
|
+
}
|
|
115
|
+
console.log(` ID: ${node.id}`);
|
|
116
|
+
console.log();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
console.log(chalk.green(`Total: ${app.nodes.length} node(s)\n`));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// SSH command
|
|
127
|
+
const sshCmd = new Command('ssh');
|
|
128
|
+
sshCmd
|
|
129
|
+
.description('SSH into an app\'s VM')
|
|
130
|
+
.argument('<app>', 'App name')
|
|
131
|
+
.option('-n, --node <node>', 'Node name or index (defaults to primary node)')
|
|
132
|
+
.action(async (appName: string, options) => {
|
|
133
|
+
const platformToken = getPlatformToken();
|
|
134
|
+
const platformClient = new PlatformClient(platformToken);
|
|
135
|
+
const uncloudRunner = new UncloudRunner(platformClient);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const app = await platformClient.getApp(appName);
|
|
139
|
+
|
|
140
|
+
if (!app) {
|
|
141
|
+
console.log(chalk.red(`\nApp '${appName}' not found\n`));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let targetNode;
|
|
146
|
+
|
|
147
|
+
if (options.node) {
|
|
148
|
+
// Check if it's a number (index) or name
|
|
149
|
+
const nodeIndex = parseInt(options.node);
|
|
150
|
+
if (!isNaN(nodeIndex) && nodeIndex > 0 && nodeIndex <= app.nodes.length) {
|
|
151
|
+
targetNode = app.nodes[nodeIndex - 1];
|
|
152
|
+
} else {
|
|
153
|
+
targetNode = app.nodes.find(n => n.name === options.node);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!targetNode) {
|
|
157
|
+
console.log(chalk.red(`\nNode '${options.node}' not found\n`));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
// Default to primary node
|
|
162
|
+
targetNode = app.nodes.find(n => n.isPrimary);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Get IP address (prefer IPv6, fall back to IPv4)
|
|
166
|
+
const nodeIp = targetNode?.ipv6 || targetNode?.ipv4;
|
|
167
|
+
if (!targetNode || !nodeIp) {
|
|
168
|
+
console.log(chalk.red(`\nTarget node not found or has no IP\n`));
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Get connection info (handles certificate and gateway tunnel if needed)
|
|
173
|
+
const connInfo = await uncloudRunner.getConnectionInfo(appName);
|
|
174
|
+
|
|
175
|
+
if (connInfo.viaGateway) {
|
|
176
|
+
console.log(chalk.cyan(`\nConnecting to ${targetNode.name} via gateway...\n`));
|
|
177
|
+
} else {
|
|
178
|
+
console.log(chalk.cyan(`\nConnecting to ${targetNode.name}...\n`));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// SSH to the VM (certificate is in SSH agent)
|
|
182
|
+
if (connInfo.viaGateway && connInfo.localPort) {
|
|
183
|
+
// Use the tunnel
|
|
184
|
+
execSync(
|
|
185
|
+
`ssh -p ${connInfo.localPort} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR root@localhost`,
|
|
186
|
+
{ stdio: 'inherit' }
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
// Direct connection
|
|
190
|
+
execSync(
|
|
191
|
+
`ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR root@${nodeIp}`,
|
|
192
|
+
{ stdio: 'inherit' }
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
} finally {
|
|
199
|
+
uncloudRunner.cleanup();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Destroy command
|
|
204
|
+
const destroyCmd = new Command('destroy');
|
|
205
|
+
destroyCmd
|
|
206
|
+
.description('Delete an app and all its infrastructure')
|
|
207
|
+
.argument('<app>', 'App name')
|
|
208
|
+
.option('--force', 'Skip confirmation')
|
|
209
|
+
.action(async (appName: string, options) => {
|
|
210
|
+
try {
|
|
211
|
+
const platformToken = getPlatformToken();
|
|
212
|
+
const platformClient = new PlatformClient(platformToken);
|
|
213
|
+
const app = await platformClient.getApp(appName);
|
|
214
|
+
|
|
215
|
+
if (!app) {
|
|
216
|
+
console.log(chalk.red(`\n❌ App '${appName}' not found\n`));
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Confirm deletion
|
|
221
|
+
if (!options.force) {
|
|
222
|
+
console.log(chalk.yellow(`\n⚠️ You are about to delete '${appName}' and all its infrastructure:`));
|
|
223
|
+
console.log(` - ${app.nodes.length} VM(s) will be deleted`);
|
|
224
|
+
console.log(` - All data will be lost\n`);
|
|
225
|
+
|
|
226
|
+
// For now, we'll skip interactive confirmation in CLI
|
|
227
|
+
// In a real implementation, you'd use a package like 'prompts'
|
|
228
|
+
console.log(chalk.red('Use --force flag to confirm deletion\n'));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const spinner = ora('Deleting infrastructure...').start();
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
// Delete all VMs via platform API
|
|
236
|
+
for (const node of app.nodes) {
|
|
237
|
+
spinner.text = `Deleting VM '${node.name}'...`;
|
|
238
|
+
await platformClient.deleteVM(app.location, node.name);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Remove app from backend
|
|
242
|
+
spinner.text = 'Removing app from database...';
|
|
243
|
+
await platformClient.deleteApp(appName);
|
|
244
|
+
|
|
245
|
+
// Remove uncloud context from local config
|
|
246
|
+
spinner.text = 'Cleaning up local configuration...';
|
|
247
|
+
const contextName = app.uncloudContext || appName;
|
|
248
|
+
removeUncloudContext(contextName);
|
|
249
|
+
|
|
250
|
+
spinner.succeed(chalk.green(`App '${appName}' destroyed successfully`));
|
|
251
|
+
|
|
252
|
+
console.log(chalk.cyan('\n💡 All infrastructure has been deleted.\n'));
|
|
253
|
+
} catch (error) {
|
|
254
|
+
spinner.fail(chalk.red('Failed to destroy app'));
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error(chalk.red(`\n❌ Error: ${(error as Error).message}\n`));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Link command
|
|
264
|
+
const linkCmd = new Command('link');
|
|
265
|
+
linkCmd
|
|
266
|
+
.description('Link current directory to an existing app')
|
|
267
|
+
.argument('<app-name>', 'App name to link to')
|
|
268
|
+
.action(async (appName: string) => {
|
|
269
|
+
try {
|
|
270
|
+
const platformToken = getPlatformToken();
|
|
271
|
+
const platformClient = new PlatformClient(platformToken);
|
|
272
|
+
|
|
273
|
+
// Check if app exists
|
|
274
|
+
const app = await platformClient.getApp(appName);
|
|
275
|
+
if (!app) {
|
|
276
|
+
console.log(chalk.red(`\nApp '${appName}' not found\n`));
|
|
277
|
+
console.log(`Run ${chalk.bold('hackerrun apps')} to see your apps.\n`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check if already linked
|
|
282
|
+
const existingConfig = readAppConfig();
|
|
283
|
+
if (existingConfig) {
|
|
284
|
+
if (existingConfig.appName === appName) {
|
|
285
|
+
console.log(chalk.yellow(`\nThis directory is already linked to '${appName}'\n`));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
console.log(chalk.yellow(`\nThis directory is currently linked to '${existingConfig.appName}'`));
|
|
289
|
+
console.log(`Updating link to '${appName}'...\n`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Create hackerrun.yaml
|
|
293
|
+
linkApp(appName);
|
|
294
|
+
|
|
295
|
+
console.log(chalk.green(`\nLinked to app '${appName}'\n`));
|
|
296
|
+
console.log(` Domain: https://${app.domainName}.hackerrun.app`);
|
|
297
|
+
console.log(` Run ${chalk.bold('hackerrun deploy')} to deploy changes.\n`);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Rename command
|
|
305
|
+
const renameCmd = new Command('rename');
|
|
306
|
+
renameCmd
|
|
307
|
+
.description('Rename an app (per-user unique)')
|
|
308
|
+
.requiredOption('--app <current-name>', 'Current app name')
|
|
309
|
+
.argument('<new-name>', 'New app name')
|
|
310
|
+
.action(async (newName: string, options) => {
|
|
311
|
+
try {
|
|
312
|
+
const platformToken = getPlatformToken();
|
|
313
|
+
const platformClient = new PlatformClient(platformToken);
|
|
314
|
+
const currentName = options.app;
|
|
315
|
+
|
|
316
|
+
// Check if app exists
|
|
317
|
+
const app = await platformClient.getApp(currentName);
|
|
318
|
+
if (!app) {
|
|
319
|
+
console.log(chalk.red(`\nApp '${currentName}' not found\n`));
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const spinner = ora(`Renaming app '${currentName}' to '${newName}'...`).start();
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const updatedApp = await platformClient.renameApp(currentName, newName);
|
|
327
|
+
spinner.succeed(chalk.green(`App renamed successfully`));
|
|
328
|
+
|
|
329
|
+
console.log(`\n Old name: ${currentName}`);
|
|
330
|
+
console.log(` New name: ${updatedApp.appName}`);
|
|
331
|
+
console.log(` Domain: https://${updatedApp.domainName}.hackerrun.app\n`);
|
|
332
|
+
|
|
333
|
+
// Update local config if linked to this app
|
|
334
|
+
const existingConfig = readAppConfig();
|
|
335
|
+
if (existingConfig?.appName === currentName) {
|
|
336
|
+
linkApp(newName);
|
|
337
|
+
console.log(chalk.dim(`Updated local hackerrun.yaml\n`));
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
spinner.fail(chalk.red('Failed to rename app'));
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Domain command
|
|
350
|
+
const domainCmd = new Command('domain');
|
|
351
|
+
domainCmd
|
|
352
|
+
.description('Change app\'s domain name (globally unique)')
|
|
353
|
+
.requiredOption('--app <app-name>', 'App name')
|
|
354
|
+
.argument('<new-domain>', 'New domain name (without .hackerrun.app)')
|
|
355
|
+
.action(async (newDomain: string, options) => {
|
|
356
|
+
try {
|
|
357
|
+
const platformToken = getPlatformToken();
|
|
358
|
+
const platformClient = new PlatformClient(platformToken);
|
|
359
|
+
const appName = options.app;
|
|
360
|
+
|
|
361
|
+
// Check if app exists
|
|
362
|
+
const app = await platformClient.getApp(appName);
|
|
363
|
+
if (!app) {
|
|
364
|
+
console.log(chalk.red(`\nApp '${appName}' not found\n`));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const oldDomain = app.domainName;
|
|
369
|
+
const spinner = ora(`Changing domain from '${oldDomain}' to '${newDomain}'...`).start();
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const updatedApp = await platformClient.renameDomain(appName, newDomain);
|
|
373
|
+
spinner.succeed(chalk.green(`Domain changed successfully`));
|
|
374
|
+
|
|
375
|
+
// Sync gateway routes with new domain
|
|
376
|
+
spinner.start('Syncing gateway routes...');
|
|
377
|
+
await platformClient.syncGatewayRoutes(app.location);
|
|
378
|
+
spinner.succeed(chalk.green('Gateway routes synced'));
|
|
379
|
+
|
|
380
|
+
console.log(`\n App: ${updatedApp.appName}`);
|
|
381
|
+
console.log(` Old domain: https://${oldDomain}.hackerrun.app`);
|
|
382
|
+
console.log(` New domain: https://${updatedApp.domainName}.hackerrun.app\n`);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
spinner.fail(chalk.red('Failed to change domain'));
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
return { appsCmd, nodesCmd, sshCmd, destroyCmd, linkCmd, renameCmd, domainCmd };
|
|
394
|
+
}
|