@chabokan.net/cli 0.8.10 → 0.8.15

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,6 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import Command from "../../base.js";
3
- import { isEmptyObject } from "../../helper.js";
3
+ import { isEmptyObject, handleApiError, logErrorDetails } from "../../helper.js";
4
4
  import chalk from "chalk";
5
5
  import inquirer from 'inquirer';
6
6
  import axios from "axios";
@@ -11,7 +11,7 @@ export default class ServiceStop extends Command {
11
11
  service: Flags.string({ char: 's', description: 'service name' }),
12
12
  };
13
13
  async run() {
14
- const { args, flags } = await this.parse(ServiceStop);
14
+ const { flags } = await this.parse(ServiceStop);
15
15
  const cli = this;
16
16
  await this.init_run();
17
17
  const config_json = await this.read_config();
@@ -21,9 +21,9 @@ export default class ServiceStop extends Command {
21
21
  return;
22
22
  }
23
23
  if (!flags.service) {
24
- let all_services = await this.get_services({ 'not_status': 'pending' });
25
- if (all_services) {
26
- let { service } = await inquirer.prompt({
24
+ const all_services = await this.get_services({ 'not_status': 'pending' });
25
+ if (all_services && all_services.length > 0) {
26
+ const { service } = await inquirer.prompt({
27
27
  type: 'list',
28
28
  message: 'Please select a service:',
29
29
  name: 'service',
@@ -31,25 +31,38 @@ export default class ServiceStop extends Command {
31
31
  });
32
32
  selected_service = service;
33
33
  }
34
+ else {
35
+ cli.log("No services available.");
36
+ return;
37
+ }
38
+ }
39
+ if (selected_service) {
40
+ await this.send_request(cli, selected_service);
34
41
  }
35
- await this.send_request(cli, selected_service);
36
42
  }
37
43
  async send_request(cli, selected_service) {
38
44
  try {
39
- if (selected_service) {
40
- const { data } = await axios.get("services/" + selected_service + "/stop/", this.axiosConfig);
41
- if (data.success) {
42
- cli.log(`${chalk.green('[Success]')} service stop successfully.`);
43
- }
45
+ const { data } = await axios.get(`services/${selected_service}/stop/`, this.axiosConfig);
46
+ if (data.success) {
47
+ cli.log(`${chalk.green('[Success]')} Service '${selected_service}' stopped successfully.`);
48
+ }
49
+ else {
50
+ cli.log(`${chalk.red('[Error]')} Failed to stop service '${selected_service}'. ${data.message || 'Please try again later.'}`);
44
51
  }
45
52
  }
46
- catch (e) {
47
- if (e.response.status == 404) {
48
- cli.log(chalk.blue("Selected Service Not Founded."));
53
+ catch (error) {
54
+ const errorInfo = handleApiError(error, 'Failed to stop service.', {
55
+ endpoint: `services/${selected_service}/stop/`,
56
+ serviceName: selected_service,
57
+ operation: 'Stop Service'
58
+ });
59
+ if (errorInfo.status === 404) {
60
+ cli.log(`${chalk.red('[Error]')} Service '${selected_service}' not found. Please verify the service name using 'chabok service list' and try again.`);
49
61
  }
50
62
  else {
51
- console.log(e.data);
63
+ cli.log(`${chalk.red('[Error]')} ${errorInfo.message}`);
52
64
  }
65
+ logErrorDetails(errorInfo, cli);
53
66
  }
54
67
  }
55
68
  }
package/dist/helper.d.ts CHANGED
@@ -1,12 +1,19 @@
1
+ import { AxiosRequestConfig } from "axios";
1
2
  import { Ignore } from 'ignore';
2
- export declare function isObject(obj: any): boolean;
3
- export declare function isEmptyObject(obj: any): any;
4
- export declare function read_config_file(): {
5
- users: {};
6
- default_user: string;
7
- };
8
- export declare function get_all_services(filter: {} | undefined, axiosConfig: any): Promise<any>;
3
+ import type { ConfigFile, Service, ErrorInfo } from './types.js';
4
+ export declare function isObject(obj: unknown): obj is Record<string, unknown>;
5
+ export declare function isEmptyObject(obj: unknown): boolean;
6
+ export declare function read_config_file(): ConfigFile;
7
+ export declare function get_all_services(filter: Record<string, string> | undefined, axiosConfig: AxiosRequestConfig): Promise<Service[]>;
9
8
  export declare function trimLines(lines: string[]): string[];
10
9
  export declare const loadIgnoreFile: (ignoreInstance: Ignore, ignoreFilePath: string, projectPath: string) => void;
11
10
  export declare function addIgnorePatterns(ignoreInstance: Ignore, projectPath: string, dir: string): void;
12
- export declare const checkUpdate: (version: any) => Promise<void>;
11
+ export declare const checkUpdate: (version: string) => Promise<void>;
12
+ export declare function handleApiError(error: unknown, defaultMessage: string, context?: {
13
+ endpoint?: string;
14
+ serviceName?: string;
15
+ operation?: string;
16
+ }): ErrorInfo;
17
+ export declare function logErrorDetails(errorInfo: ErrorInfo, command: {
18
+ log: (msg: string) => void;
19
+ }): void;
package/dist/helper.js CHANGED
@@ -8,10 +8,10 @@ import semver from 'semver';
8
8
  import pkgJson from 'package-json';
9
9
  import semverDiff from 'semver-diff';
10
10
  export function isObject(obj) {
11
- return obj != null && obj.constructor.name === "Object";
11
+ return obj != null && typeof obj === 'object' && obj.constructor?.name === "Object";
12
12
  }
13
13
  export function isEmptyObject(obj) {
14
- return obj && obj.constructor === Object && Object.keys(obj).length === 0;
14
+ return obj !== null && typeof obj === 'object' && obj.constructor === Object && Object.keys(obj).length === 0;
15
15
  }
16
16
  export function read_config_file() {
17
17
  let config_json = {
@@ -19,35 +19,71 @@ export function read_config_file() {
19
19
  default_user: ""
20
20
  };
21
21
  try {
22
- config_json = JSON.parse(fs.readFileSync(GLOBAL_CONF_PATH).toString('utf-8')) || {};
22
+ if (!fs.existsSync(GLOBAL_CONF_PATH)) {
23
+ return config_json;
24
+ }
25
+ const fileContent = fs.readFileSync(GLOBAL_CONF_PATH).toString('utf-8');
26
+ if (!fileContent.trim()) {
27
+ return config_json;
28
+ }
29
+ const parsed = JSON.parse(fileContent);
30
+ if (isObject(parsed) && 'users' in parsed && 'default_user' in parsed) {
31
+ config_json = {
32
+ users: parsed.users || {},
33
+ default_user: String(parsed.default_user || "")
34
+ };
35
+ }
23
36
  }
24
- catch {
37
+ catch (error) {
38
+ if (process.env.CHABOK_DEBUG === "true") {
39
+ console.error(`Error reading config file (${GLOBAL_CONF_PATH}):`, error);
40
+ }
41
+ // File doesn't exist or invalid JSON, return default
25
42
  }
26
43
  return config_json;
27
44
  }
28
45
  export async function get_all_services(filter = {}, axiosConfig) {
29
- let all_services = [];
46
+ const all_services = [];
30
47
  try {
31
48
  let url_filter = "";
32
49
  Object.keys(filter).forEach((item, index) => {
33
- let key = item;
34
- // @ts-ignore
35
- let value = filter[item];
36
- if (index == 0) {
37
- url_filter += `?${key}=${value}`;
50
+ const key = item;
51
+ const value = filter[item];
52
+ if (index === 0) {
53
+ url_filter += `?${key}=${encodeURIComponent(value)}`;
38
54
  }
39
55
  else {
40
- url_filter += `&${key}=${value}`;
56
+ url_filter += `&${key}=${encodeURIComponent(value)}`;
41
57
  }
42
58
  });
43
- const { data } = await axios.get("services/" + url_filter, axiosConfig);
44
- data.services.forEach(function (item) {
45
- // @ts-ignore
46
- all_services.push({ "value": item.main_name, "name": `${item.main_name} (${item.platform.name})` });
47
- });
59
+ const endpoint = "services/" + url_filter;
60
+ const { data } = await axios.get(endpoint, axiosConfig);
61
+ if (data.services && Array.isArray(data.services)) {
62
+ data.services.forEach((item) => {
63
+ all_services.push({
64
+ value: item.main_name,
65
+ name: `${item.main_name} (${item.platform.name})`
66
+ });
67
+ });
68
+ }
48
69
  }
49
- catch (e) {
50
- console.log(e.response.data);
70
+ catch (error) {
71
+ const errorInfo = handleApiError(error, 'Failed to fetch services list.', {
72
+ endpoint: 'services/',
73
+ operation: 'Fetch services'
74
+ });
75
+ if (process.env.CHABOK_DEBUG === "true") {
76
+ console.error('Error fetching services:', errorInfo);
77
+ if (axios.isAxiosError(error)) {
78
+ console.error('Full error:', error);
79
+ }
80
+ }
81
+ else {
82
+ // Only log critical errors in non-debug mode
83
+ if (errorInfo.status && errorInfo.status >= 500) {
84
+ console.error(`Error fetching services: ${errorInfo.message}`);
85
+ }
86
+ }
51
87
  }
52
88
  return all_services;
53
89
  }
@@ -89,25 +125,185 @@ export function addIgnorePatterns(ignoreInstance, projectPath, dir) {
89
125
  }
90
126
  export const checkUpdate = async (version) => {
91
127
  const name = "@chabokan.net/cli";
92
- const { version: latestVersion } = await pkgJson(name);
93
- // check if local package version is less than the remote version
94
- const updateAvailable = semver.lt(version, latestVersion);
95
- if (updateAvailable) {
96
- let updateType = '';
97
- // check the type of version difference which is usually patch, minor, major etc.
98
- let verDiff = semverDiff(version, latestVersion);
99
- if (verDiff) {
100
- updateType = verDiff;
101
- }
102
- const msg = {
103
- updateAvailable: `${updateType} update available ${chalk.dim(version)} → ${chalk.green(latestVersion)}`,
104
- runUpdate: `Run ${chalk.cyan(`npm i -g ${name}`)} to update`,
105
- };
106
- // notify the user about the available udpate
107
- console.log(boxen(`${msg.updateAvailable}\n${msg.runUpdate}`, {
108
- margin: 1,
109
- padding: 1,
110
- align: 'center',
111
- }));
128
+ try {
129
+ const { version: latestVersion } = await pkgJson(name);
130
+ // check if local package version is less than the remote version
131
+ const updateAvailable = semver.lt(version, latestVersion);
132
+ if (updateAvailable) {
133
+ let updateType = '';
134
+ // check the type of version difference which is usually patch, minor, major etc.
135
+ const verDiff = semverDiff(version, latestVersion);
136
+ if (verDiff) {
137
+ updateType = verDiff;
138
+ }
139
+ const msg = {
140
+ updateAvailable: `${updateType} update available ${chalk.dim(version)} ${chalk.green(latestVersion)}`,
141
+ runUpdate: `Run ${chalk.cyan(`npm i -g ${name}`)} to update`,
142
+ };
143
+ // notify the user about the available update
144
+ console.log(boxen(`${msg.updateAvailable}\n${msg.runUpdate}`, {
145
+ margin: 1,
146
+ padding: 1,
147
+ align: 'center',
148
+ }));
149
+ }
150
+ }
151
+ catch (error) {
152
+ // Silently fail if update check fails (network issues, etc.)
153
+ if (process.env.CHABOK_DEBUG === "true") {
154
+ console.error('Failed to check for updates:', error);
155
+ }
112
156
  }
113
157
  };
158
+ export function handleApiError(error, defaultMessage, context) {
159
+ const errorInfo = {
160
+ message: defaultMessage,
161
+ };
162
+ if (context?.endpoint) {
163
+ errorInfo.endpoint = context.endpoint;
164
+ }
165
+ if (axios.isAxiosError(error)) {
166
+ // Extract endpoint from error config
167
+ if (error.config?.url) {
168
+ errorInfo.endpoint = error.config.url;
169
+ }
170
+ if (error.config?.baseURL) {
171
+ errorInfo.endpoint = `${error.config.baseURL}${errorInfo.endpoint || ''}`;
172
+ }
173
+ if (error.response) {
174
+ const status = error.response.status;
175
+ const data = error.response.data;
176
+ errorInfo.status = status;
177
+ // Build detailed error message based on status code
178
+ let message = defaultMessage;
179
+ switch (status) {
180
+ case 400:
181
+ message = "Bad Request: Invalid parameters provided.";
182
+ break;
183
+ case 401:
184
+ message = "Authentication failed: Please login again using 'chabok login'.";
185
+ break;
186
+ case 403:
187
+ message = "Access denied: You don't have permission to perform this action.";
188
+ break;
189
+ case 404:
190
+ if (context?.serviceName) {
191
+ message = `Service '${context.serviceName}' not found. Please check the service name and try again.`;
192
+ }
193
+ else if (context?.operation) {
194
+ message = `${context.operation} not found. Please verify the information and try again.`;
195
+ }
196
+ else {
197
+ message = "Resource not found. Please verify the information and try again.";
198
+ }
199
+ break;
200
+ case 409:
201
+ message = "Conflict: The resource already exists or is in an invalid state.";
202
+ break;
203
+ case 422:
204
+ message = "Validation error: The provided data is invalid.";
205
+ break;
206
+ case 429:
207
+ message = "Rate limit exceeded: Too many requests. Please try again later.";
208
+ break;
209
+ case 500:
210
+ message = "Server error: The server encountered an internal error. Please try again later.";
211
+ break;
212
+ case 502:
213
+ message = "Bad Gateway: The server is temporarily unavailable. Please try again later.";
214
+ break;
215
+ case 503:
216
+ message = "Service unavailable: The service is temporarily down. Please try again later.";
217
+ break;
218
+ default:
219
+ message = defaultMessage;
220
+ }
221
+ // Try to extract detailed error message from response
222
+ if (typeof data === 'object' && data !== null) {
223
+ if ('message' in data && typeof data.message === 'string') {
224
+ errorInfo.details = data.message;
225
+ message = `${message} ${data.message}`;
226
+ }
227
+ else if ('error' in data && typeof data.error === 'string') {
228
+ errorInfo.details = data.error;
229
+ message = `${message} ${data.error}`;
230
+ }
231
+ else if ('detail' in data && typeof data.detail === 'string') {
232
+ errorInfo.details = data.detail;
233
+ message = `${message} ${data.detail}`;
234
+ }
235
+ else if (Array.isArray(data) && data.length > 0) {
236
+ errorInfo.details = JSON.stringify(data);
237
+ message = `${message} ${JSON.stringify(data)}`;
238
+ }
239
+ else if (Object.keys(data).length > 0) {
240
+ errorInfo.details = JSON.stringify(data);
241
+ }
242
+ }
243
+ else if (typeof data === 'string') {
244
+ errorInfo.details = data;
245
+ message = `${message} ${data}`;
246
+ }
247
+ errorInfo.message = message;
248
+ errorInfo.code = error.code || `HTTP_${status}`;
249
+ }
250
+ else if (error.request) {
251
+ // Request was made but no response received
252
+ if (error.code === 'ECONNREFUSED') {
253
+ errorInfo.message = "Connection refused: Unable to connect to the server. Please check your internet connection and try again.";
254
+ errorInfo.code = 'ECONNREFUSED';
255
+ }
256
+ else if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') {
257
+ errorInfo.message = "Request timeout: The server took too long to respond. Please check your connection and try again.";
258
+ errorInfo.code = error.code;
259
+ }
260
+ else if (error.code === 'ENOTFOUND') {
261
+ errorInfo.message = "DNS error: Unable to resolve server address. Please check your internet connection.";
262
+ errorInfo.code = 'ENOTFOUND';
263
+ }
264
+ else {
265
+ errorInfo.message = "Network error: Unable to reach the server. Please check your internet connection and try again.";
266
+ errorInfo.code = error.code || 'NETWORK_ERROR';
267
+ }
268
+ }
269
+ else {
270
+ // Error in request setup
271
+ errorInfo.message = `Request setup error: ${error.message || defaultMessage}`;
272
+ errorInfo.code = error.code || 'REQUEST_ERROR';
273
+ }
274
+ }
275
+ else if (error instanceof Error) {
276
+ errorInfo.message = error.message || defaultMessage;
277
+ errorInfo.code = error.code || 'UNKNOWN_ERROR';
278
+ errorInfo.details = error.stack;
279
+ }
280
+ else if (typeof error === 'string') {
281
+ errorInfo.message = error;
282
+ }
283
+ // Add context information to message if available
284
+ if (context?.serviceName && !errorInfo.message.includes(context.serviceName)) {
285
+ errorInfo.message = `[Service: ${context.serviceName}] ${errorInfo.message}`;
286
+ }
287
+ if (context?.operation && !errorInfo.message.includes(context.operation)) {
288
+ errorInfo.message = `[Operation: ${context.operation}] ${errorInfo.message}`;
289
+ }
290
+ return errorInfo;
291
+ }
292
+ export function logErrorDetails(errorInfo, command) {
293
+ if (process.env.CHABOK_DEBUG === "true") {
294
+ command.log(chalk.dim('--- Error Details ---'));
295
+ if (errorInfo.status) {
296
+ command.log(chalk.dim(`Status Code: ${errorInfo.status}`));
297
+ }
298
+ if (errorInfo.endpoint) {
299
+ command.log(chalk.dim(`Endpoint: ${errorInfo.endpoint}`));
300
+ }
301
+ if (errorInfo.code) {
302
+ command.log(chalk.dim(`Error Code: ${errorInfo.code}`));
303
+ }
304
+ if (errorInfo.details) {
305
+ command.log(chalk.dim(`Details: ${errorInfo.details}`));
306
+ }
307
+ command.log(chalk.dim('--- End Error Details ---'));
308
+ }
309
+ }
@@ -0,0 +1,57 @@
1
+ export interface ConfigFile {
2
+ users: Record<string, UserConfig>;
3
+ default_user: string;
4
+ }
5
+ export interface UserConfig {
6
+ token: string;
7
+ }
8
+ export interface Service {
9
+ value: string;
10
+ name: string;
11
+ }
12
+ export interface ApiResponse<T = unknown> {
13
+ success: boolean;
14
+ data?: T;
15
+ message?: string;
16
+ [key: string]: unknown;
17
+ }
18
+ export interface LoginResponse {
19
+ success: boolean;
20
+ token?: string;
21
+ }
22
+ export interface UserInfo {
23
+ user: {
24
+ email: string;
25
+ [key: string]: unknown;
26
+ };
27
+ }
28
+ export interface ServiceData {
29
+ main_name: string;
30
+ platform: {
31
+ name: string;
32
+ };
33
+ [key: string]: unknown;
34
+ }
35
+ export interface ServicesResponse {
36
+ services: ServiceData[];
37
+ }
38
+ export interface ChabokFile {
39
+ service?: string;
40
+ [key: string]: unknown;
41
+ }
42
+ export interface AxiosErrorResponse {
43
+ response?: {
44
+ status: number;
45
+ data?: unknown;
46
+ [key: string]: unknown;
47
+ };
48
+ data?: unknown;
49
+ message?: string;
50
+ }
51
+ export interface ErrorInfo {
52
+ message: string;
53
+ status?: number;
54
+ endpoint?: string;
55
+ details?: string;
56
+ code?: string;
57
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // Type definitions for the CLI
2
+ export {};
@@ -430,5 +430,5 @@
430
430
  ]
431
431
  }
432
432
  },
433
- "version": "0.8.10"
433
+ "version": "0.8.15"
434
434
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chabokan.net/cli",
3
- "version": "0.8.10",
3
+ "version": "0.8.15",
4
4
  "description": "Chabokan Cli for PaaS Services",
5
5
  "author": "Mohammad Abdi",
6
6
  "bin": {