@nestbox-ai/cli 1.0.6 → 1.0.8

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.
@@ -1,147 +1,439 @@
1
1
  import { Command } from "commander";
2
- import { getAuthToken } from "../utils/auth";
3
- import { Configuration, ProjectsApi, MachineInstancesApi } from "@nestbox-ai/admin";
4
- import { readNestboxConfig } from "./projects";
5
2
  import chalk from "chalk";
3
+ import ora from "ora";
6
4
  import Table from "cli-table3";
5
+ import { getAuthToken } from "../utils/auth";
6
+ import { Configuration, ProjectsApi, MachineInstancesApi, MiscellaneousApi } from "@nestbox-ai/admin";
7
+ import { resolveProject } from "../utils/project";
8
+ import inquirer from "inquirer";
9
+ import axios from "axios";
10
+ import { userData } from "../utils/user";
7
11
 
8
12
  export function registerComputeProgram(program: Command): void {
9
-
10
13
  const authToken = getAuthToken();
14
+
15
+ if (!authToken) {
16
+ console.error(chalk.red('No authentication token found. Please login first.'));
17
+ return;
18
+ }
19
+
11
20
  const configuration = new Configuration({
12
- basePath: authToken?.serverUrl,
21
+ basePath: authToken.serverUrl,
13
22
  baseOptions: {
14
23
  headers: {
15
- "Authorization": authToken?.token,
24
+ "Authorization": authToken.token,
16
25
  }
17
26
  }
18
27
  });
19
28
 
20
29
  const machineInstanceApi = new MachineInstancesApi(configuration);
30
+ const miscellaneousApi = new MiscellaneousApi(configuration);
21
31
  const projectsApi = new ProjectsApi(configuration);
22
32
 
23
- // Create the main project command
24
- const projectCommand = program
33
+ // Create the main compute command
34
+ const computeCommand = program
25
35
  .command('compute')
26
36
  .description('Manage Nestbox computes');
27
37
 
28
- projectCommand
38
+ // List command
39
+ computeCommand
29
40
  .command('list')
30
- .description('list all instances')
31
- .action(() => {
41
+ .description('List all compute instances')
42
+ .option('--project <projectId>', 'Project ID or name (defaults to the current project)')
43
+ .action(async (options) => {
32
44
  try {
33
- if (!authToken) {
34
- console.error(chalk.red('No authentication token found. Please login first.'));
35
- return;
45
+ // Resolve project using the shared utility
46
+ const project = await resolveProject(projectsApi, options);
47
+
48
+ const spinner = ora(`Fetching compute instances for project: ${project.name}`).start();
49
+
50
+ try {
51
+ // Fetch machine instances for the project
52
+ const instancesResponse: any = await machineInstanceApi.machineInstancesControllerGetMachineInstanceByUserId(
53
+ project.id,
54
+ 0, // page
55
+ 10 // limit
56
+ );
57
+
58
+ spinner.succeed('Successfully retrieved compute instances');
59
+
60
+ const instances = instancesResponse.data?.machineInstances || [];
61
+
62
+ if (instances.length === 0) {
63
+ console.log(chalk.yellow('No compute instances found for this project.'));
64
+ return;
65
+ }
66
+
67
+ // Create table for display
68
+ const table = new Table({
69
+ head: [
70
+ chalk.white.bold('ID'),
71
+ chalk.white.bold('Name'),
72
+ chalk.white.bold('Status'),
73
+ chalk.white.bold('API Key')
74
+ ],
75
+ style: {
76
+ head: [], // Disable the default styling
77
+ border: []
78
+ }
79
+ });
80
+
81
+ // Status mappings
82
+ const statusMappings: any = {
83
+ 'Job Scheduled': 'Scheduled',
84
+ 'Job Executed': 'Ready',
85
+ 'Job in Progress': 'Initializing',
86
+ 'Job Failed': 'Failed',
87
+ 'Deleting': 'Deleting',
88
+ };
89
+
90
+ // Add rows to the table
91
+ instances.forEach((instance: any) => {
92
+ // Map the status if a mapping exists
93
+ const originalStatus = instance.runningStatus || 'unknown';
94
+ const displayStatus = statusMappings[originalStatus] || originalStatus;
95
+
96
+ // Color the status based on its mapped value
97
+ let statusColor;
98
+
99
+ switch(displayStatus.toLowerCase()) {
100
+ case 'ready':
101
+ statusColor = chalk.green(displayStatus);
102
+ break;
103
+ case 'failed':
104
+ statusColor = chalk.red(displayStatus);
105
+ break;
106
+ case 'initializing':
107
+ statusColor = chalk.yellow(displayStatus);
108
+ break;
109
+ case 'scheduled':
110
+ statusColor = chalk.blue(displayStatus);
111
+ break;
112
+ case 'deleting':
113
+ statusColor = chalk.red(displayStatus);
114
+ break;
115
+ default:
116
+ statusColor = chalk.gray(displayStatus);
117
+ }
118
+
119
+ table.push([
120
+ instance.id || 'N/A',
121
+ instance.instanceName || 'N/A',
122
+ statusColor,
123
+ instance.instanceApiKey || 'N/A'
124
+ ]);
125
+ });
126
+
127
+ // Display the table
128
+ console.log(table.toString());
129
+
130
+ } catch (error: any) {
131
+ spinner.fail('Failed to retrieve compute instances');
132
+ if (error.response) {
133
+ console.error(chalk.red('API Error:'), error.response.data?.message || 'Unknown error');
134
+ } else {
135
+ console.error(chalk.red('Error:'), error.message || 'Unknown error');
136
+ }
36
137
  }
37
- const config = readNestboxConfig();
38
-
39
- // Set the default project
40
- config.projects = config.projects || {};
138
+ } catch (error: any) {
139
+ console.error(chalk.red('Error:'), error.message || 'Unknown error');
140
+ }
141
+ });
41
142
 
42
- // Get the default project name
43
- const defaultProjectName = config.projects.default;
44
143
 
45
- if (!defaultProjectName) {
46
- console.log(chalk.yellow('No default project set. Use "nestbox project set-default <project-name>" to set a default project.'));
47
- return;
144
+ computeCommand
145
+ .command('create')
146
+ .description('Create a new compute instance')
147
+ .requiredOption('--image <imageId>', 'Image ID to use for the compute instance')
148
+ .option('--project <projectId>', 'Project ID or name (defaults to the current project)')
149
+ .action(async (options) => {
150
+ try {
151
+ // Resolve project using the shared utility
152
+ const project = await resolveProject(projectsApi, options);
153
+
154
+ const spinner = ora(`Fetching available images...`).start();
155
+
156
+ try {
157
+ // Get available images data
158
+ const response = await miscellaneousApi.miscellaneousControllerGetData();
159
+ const availableImages: any = response.data || [];
160
+
161
+ // Find the selected image
162
+ const selectedImage = availableImages.find((image: any) => image.id === options.image);
163
+
164
+ if (!selectedImage) {
165
+ spinner.fail(`Image ID '${options.image}' does not exist.`);
166
+ console.log(chalk.yellow('Available image IDs:'));
167
+ availableImages.forEach((img: any) => {
168
+ console.log(` ${chalk.cyan(img.id)} - ${img.name} (${img.type})`);
169
+ });
170
+ return;
171
+ }
172
+
173
+ spinner.succeed(`Found image: ${selectedImage.name}`);
174
+
175
+ // Ask for instance name first
176
+ const instanceNameAnswer = await inquirer.prompt([
177
+ {
178
+ type: 'input',
179
+ name: 'instanceName',
180
+ message: 'Enter a name for this compute instance:',
181
+ validate: (input: string) => {
182
+ if (!input || input.trim() === '') {
183
+ return 'Instance name cannot be empty';
184
+ }
185
+ // Add more validation here if needed (e.g., checking for valid characters)
186
+ return true;
187
+ }
48
188
  }
189
+ ]);
190
+
191
+ const instanceName = instanceNameAnswer.instanceName;
192
+
193
+ // Directly create the provisioningParams object with the structure needed for API
194
+ let provisioningParams: Record<string, any> = {};
195
+
196
+ if (selectedImage.provisioningParameters &&
197
+ selectedImage.provisioningParameters.properties) {
198
+
199
+ const requiredParams = selectedImage.provisioningParameters.required || [];
200
+ const paramProperties = selectedImage.provisioningParameters.properties;
201
+
202
+ // Build questions for inquirer
203
+ const questions = [];
204
+
205
+ for (const [paramName, paramConfig] of Object.entries(paramProperties) as any) {
206
+ const isRequired = requiredParams.includes(paramName);
207
+ const paramUI = selectedImage.provisioningParametersUI?.[paramName] || {};
208
+
209
+ let question: any = {
210
+ name: paramName,
211
+ message: paramUI['ui:title'] || `Enter ${paramName}:`,
212
+ when: isRequired // Only ask required params by default
213
+ };
214
+
215
+ // Handle different parameter types
216
+ if (paramConfig.type === 'string') {
217
+ if (paramConfig.enum) {
218
+ question.type = 'list';
219
+ question.choices = paramConfig.enum;
220
+ } else if (paramUI['ui:widget'] === 'password') {
221
+ question.type = 'password';
222
+ } else {
223
+ question.type = 'input';
224
+ }
225
+ } else if (paramConfig.type === 'array') {
226
+ if (paramConfig.items && paramConfig.items.enum) {
227
+ question.type = 'checkbox';
228
+ question.choices = paramConfig.items.enum;
229
+ // Ensure at least one option is selected for required array parameters
230
+ question.validate = (input: any[]) => {
231
+ if (isRequired && (!input || input.length === 0)) {
232
+ return 'Please select at least one option';
233
+ }
234
+ return true;
235
+ };
236
+ }
237
+ }
238
+
239
+ if (paramUI['ui:help']) {
240
+ question.message += ` (${paramUI['ui:help']})`;
241
+ }
242
+
243
+ questions.push(question);
244
+ }
245
+
246
+ if (questions.length > 0) {
247
+ console.log(chalk.blue(`\nPlease provide the required parameters for ${selectedImage.name}:`));
248
+ const answers = await inquirer.prompt(questions);
249
+
250
+ // Assign the answers directly to provisioningParams
251
+ for (const [key, value] of Object.entries(answers)) {
252
+ provisioningParams[key] = value;
253
+ }
254
+ }
255
+ }
256
+
257
+ // Now we have all the required params in provisioningParams
258
+ console.log(chalk.green(`\nCreating compute instance '${instanceName}' with image '${selectedImage.name}'`));
259
+ console.log(chalk.dim('Using the following parameters:'));
260
+
261
+ for (const [key, value] of Object.entries(provisioningParams)) {
262
+ console.log(chalk.dim(` ${key}: ${Array.isArray(value) ? value.join(', ') : value}`));
263
+ }
264
+
265
+ // Start a spinner for the creation process
266
+ const creationSpinner = ora('Creating compute instance...').start();
49
267
 
50
- console.log(chalk.blue(`Fetching compute instances for project: ${defaultProjectName}`));
51
-
52
- // Call API to get all projects
53
- const response = projectsApi.projectControllerGetAllProjects();
268
+ const getUserData = await userData();
54
269
 
55
- response.then((response) => {
56
- // Check if the response contains projects
57
- if (response.data && response.data.data && response.data.data.projects && response.data.data.projects.length > 0) {
58
- const defaultProject = response.data.data.projects.find(
59
- (project) => project.name === defaultProjectName
60
- );
61
-
62
- if (defaultProject) {
63
- const machineInstancesResponse = machineInstanceApi.machineInstancesControllerGetMachineInstanceByUserId(
64
- defaultProject.id,
65
- 0,
66
- 10
67
- );
68
-
69
- machineInstancesResponse.then((instancesResponse: any) => {
70
- if (instancesResponse.data && instancesResponse.data.machineInstances) {
71
- const instances = instancesResponse.data.machineInstances;
72
-
73
- if (instances.length === 0) {
74
- console.log(chalk.yellow('No compute instances found for this project.'));
75
- return;
76
- }
77
-
78
- const table = new Table({
79
- head: [
80
- chalk.white.bold('ID'),
81
- chalk.white.bold('Name'),
82
- chalk.white.bold('Status'),
83
- chalk.white.bold('API Key')
84
- ],
85
- style: {
86
- head: [], // Disable the default styling
87
- border: []
88
- }
89
- });
90
-
91
- // Add rows to the table
92
- instances.forEach((instance: any) => {
93
- // Color the status based on its value
94
- let statusColor;
95
- const status = instance.runningStatus?.toLowerCase() || 'unknown';
96
-
97
- switch(true) {
98
- case status.includes('executed'):
99
- statusColor = chalk.green(instance.runningStatus);
100
- break;
101
- case status.includes('failed') || status.includes('error'):
102
- statusColor = chalk.red(instance.runningStatus);
103
- break;
104
- case status.includes('running') || status.includes('started'):
105
- statusColor = chalk.green(instance.runningStatus);
106
- break;
107
- case status.includes('stopped'):
108
- statusColor = chalk.red(instance.runningStatus);
109
- break;
110
- case status.includes('starting') || status.includes('pending'):
111
- statusColor = chalk.yellow(instance.runningStatus);
112
- break;
113
- default:
114
- statusColor = chalk.gray(instance.runningStatus || 'unknown');
115
- }
116
-
117
- table.push([
118
- instance.id || 'N/A',
119
- instance.instanceName || 'N/A',
120
- statusColor,
121
- instance.instanceApiKey || 'N/A'
122
- ]);
123
- });
124
-
125
- // Display the table
126
- console.log(table.toString());
127
- } else {
128
- console.log(chalk.yellow('No compute instance data returned from the API.'));
129
- }
130
- }).catch((error) => {
131
- console.error(chalk.red('Error fetching compute instances:'), error instanceof Error ? error.message : 'Unknown error');
132
- });
133
- } else {
134
- console.log(chalk.yellow(`Default project "${defaultProjectName}" not found among available projects.`));
270
+ try {
271
+ const createParams = {
272
+ userId: getUserData.id,
273
+ machineId: selectedImage.id,
274
+ machineTitle: selectedImage.name,
275
+ instanceName: instanceName,
276
+ ...provisioningParams
277
+ };
278
+
279
+ const createResponse = await axios.post(
280
+ `${authToken.serverUrl}/projects/${project.id}/instances`,
281
+ createParams,
282
+ {
283
+ headers: {
284
+ Authorization: authToken.token,
285
+ },
135
286
  }
136
- } else {
137
- console.log(chalk.yellow('No projects found.'));
138
- }
139
- }).catch((error) => {
140
- console.error(chalk.red('Error fetching projects:'), error instanceof Error ? error.message : 'Unknown error');
141
- });
287
+ );
288
+ creationSpinner.succeed('Compute instance created successfully!');
289
+
290
+
291
+ console.log(chalk.green(`\nInstance '${instanceName}' is now being provisioned`));
292
+ console.log(chalk.gray('You can check the status with: nestbox compute list'));
293
+ console.log(chalk.green("Instance created successfully!"));
294
+ } catch (createError: any) {
295
+ creationSpinner.fail('Failed to create compute instance');
296
+ if (createError.response) {
297
+ console.error(chalk.red('API Error:'), createError.response.data?.message || 'Unknown error');
298
+ } else {
299
+ console.error(chalk.red('Error:'), createError.message || 'Unknown error');
300
+ }
301
+ }
302
+
303
+ } catch (error: any) {
304
+ spinner.fail('Failed to fetch available images');
305
+ if (error.response) {
306
+ console.error(chalk.red('API Error:'), error.response.data?.message || 'Unknown error');
307
+ } else {
308
+ console.error(chalk.red('Error:'), error.message || 'Unknown error');
309
+ }
310
+ }
311
+ } catch (error: any) {
312
+ console.error(chalk.red('Error:'), error.message || 'Unknown error');
313
+ }
314
+ });
142
315
 
143
- } catch (error) {
144
- console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error');
316
+
317
+ computeCommand
318
+ .command('delete')
319
+ .description('Delete one or more compute instances')
320
+ .option('--project <projectId>', 'Project ID or name (defaults to the current project)')
321
+ .option('--force', 'Skip confirmation prompt')
322
+ .action(async (options) => {
323
+ try {
324
+ // Resolve project using the shared utility
325
+ const project = await resolveProject(projectsApi, options);
326
+
327
+ const spinner = ora(`Fetching compute instances for project: ${project.name}`).start();
328
+
329
+ try {
330
+ // Fetch machine instances for the project
331
+ const instancesResponse: any= await machineInstanceApi.machineInstancesControllerGetMachineInstanceByUserId(
332
+ project.id,
333
+ 0, // page
334
+ 50 // limit - increased to show more instances
335
+ );
336
+
337
+ spinner.succeed('Successfully retrieved compute instances');
338
+
339
+ const instances = instancesResponse.data?.machineInstances || [];
340
+
341
+ if (instances.length === 0) {
342
+ console.log(chalk.yellow('No compute instances found for this project.'));
343
+ return;
344
+ }
345
+
346
+ // Create choices for the selection prompt
347
+ const instanceChoices = instances.map((instance: any) => ({
348
+ name: `${instance.instanceName || 'Unnamed'} (${instance.id})`,
349
+ value: instance.id,
350
+ short: instance.instanceName || instance.id
351
+ }));
352
+
353
+ // Prompt user to select instances to delete
354
+ const { selectedInstances } = await inquirer.prompt([
355
+ {
356
+ type: 'checkbox',
357
+ name: 'selectedInstances',
358
+ message: 'Select compute instances to delete:',
359
+ choices: instanceChoices,
360
+ validate: (input) => {
361
+ if (input.length === 0) {
362
+ return 'Please select at least one instance to delete';
363
+ }
364
+ return true;
365
+ }
366
+ }
367
+ ]);
368
+
369
+ if (selectedInstances.length === 0) {
370
+ console.log(chalk.yellow('No instances selected for deletion.'));
371
+ return;
372
+ }
373
+
374
+ // Show selected instances
375
+ console.log(chalk.yellow('\nSelected instances for deletion:'));
376
+ const selectedInstanceDetails = instances
377
+ .filter((instance: any) => selectedInstances.includes(instance.id))
378
+ .map((instance: any) => ` - ${chalk.cyan(instance.instanceName || 'Unnamed')} (${instance.id})`);
379
+
380
+ console.log(selectedInstanceDetails.join('\n'));
381
+
382
+ // Confirm deletion if not using --force
383
+ if (!options.force) {
384
+ const { confirmDeletion } = await inquirer.prompt([
385
+ {
386
+ type: 'confirm',
387
+ name: 'confirmDeletion',
388
+ message: chalk.red('Are you sure you want to delete these instances? This cannot be undone.'),
389
+ default: false
390
+ }
391
+ ]);
392
+
393
+ if (!confirmDeletion) {
394
+ console.log(chalk.yellow('Deletion cancelled.'));
395
+ return;
396
+ }
397
+ }
398
+
399
+ // Process deletion - using single request with all selected IDs
400
+ const deleteSpinner = ora(`Deleting ${selectedInstances.length} instance(s)...`).start();
401
+
402
+ try {
403
+ await axios.delete(
404
+ `${authToken.serverUrl}/projects/${project.id}/instances`,
405
+ {
406
+ data: { ids: selectedInstances },
407
+ headers: {
408
+ Authorization: authToken.token,
409
+ }
410
+ }
411
+ );
412
+
413
+ deleteSpinner.succeed(`Successfully deleted ${selectedInstances.length} instance(s)`);
414
+
415
+ console.log(chalk.green('\nAll selected instances have been deleted'));
416
+ console.log(chalk.gray('You can verify with: nestbox compute list'));
417
+
418
+ } catch (error: any) {
419
+ deleteSpinner.fail(`Failed to delete instances`);
420
+ if (error.response) {
421
+ console.error(chalk.red('API Error:'), error.response.data?.message || 'Unknown error');
422
+ } else {
423
+ console.error(chalk.red('Error:'), error.message || 'Unknown error');
424
+ }
425
+ }
426
+
427
+ } catch (error: any) {
428
+ spinner.fail('Failed to retrieve compute instances');
429
+ if (error.response) {
430
+ console.error(chalk.red('API Error:'), error.response.data?.message || 'Unknown error');
431
+ } else {
432
+ console.error(chalk.red('Error:'), error.message || 'Unknown error');
433
+ }
145
434
  }
435
+ } catch (error: any) {
436
+ console.error(chalk.red('Error:'), error.message || 'Unknown error');
437
+ }
146
438
  });
147
439
  }