@chabokan.net/cli 0.8.10 → 0.9.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.
Files changed (55) hide show
  1. package/README.md +520 -66
  2. package/dist/base.d.ts +34 -9
  3. package/dist/base.js +172 -20
  4. package/dist/commands/account/info.d.ts +12 -0
  5. package/dist/commands/account/info.js +40 -0
  6. package/dist/commands/account/list.d.ts +5 -1
  7. package/dist/commands/account/list.js +35 -16
  8. package/dist/commands/account/remove.d.ts +5 -1
  9. package/dist/commands/account/remove.js +30 -15
  10. package/dist/commands/account/use.d.ts +6 -2
  11. package/dist/commands/account/use.js +26 -15
  12. package/dist/commands/cloudserver/create.d.ts +40 -0
  13. package/dist/commands/cloudserver/create.js +242 -0
  14. package/dist/commands/cloudserver/delete.d.ts +14 -0
  15. package/dist/commands/cloudserver/delete.js +65 -0
  16. package/dist/commands/cloudserver/list.d.ts +12 -0
  17. package/dist/commands/cloudserver/list.js +46 -0
  18. package/dist/commands/cloudserver/restart.d.ts +13 -0
  19. package/dist/commands/cloudserver/restart.js +51 -0
  20. package/dist/commands/cloudserver/start.d.ts +13 -0
  21. package/dist/commands/cloudserver/start.js +51 -0
  22. package/dist/commands/cloudserver/stop.d.ts +13 -0
  23. package/dist/commands/cloudserver/stop.js +52 -0
  24. package/dist/commands/deploy.d.ts +16 -5
  25. package/dist/commands/deploy.js +182 -101
  26. package/dist/commands/login.d.ts +8 -4
  27. package/dist/commands/login.js +97 -49
  28. package/dist/commands/service/domain/add.d.ts +15 -0
  29. package/dist/commands/service/domain/add.js +74 -0
  30. package/dist/commands/service/domain/remove.d.ts +15 -0
  31. package/dist/commands/service/domain/remove.js +72 -0
  32. package/dist/commands/service/list.d.ts +5 -1
  33. package/dist/commands/service/list.js +41 -20
  34. package/dist/commands/service/logs.d.ts +7 -3
  35. package/dist/commands/service/logs.js +46 -34
  36. package/dist/commands/service/resize.d.ts +10 -6
  37. package/dist/commands/service/resize.js +94 -72
  38. package/dist/commands/service/restart.d.ts +7 -3
  39. package/dist/commands/service/restart.js +40 -31
  40. package/dist/commands/service/start.d.ts +7 -3
  41. package/dist/commands/service/start.js +41 -30
  42. package/dist/commands/service/stop.d.ts +7 -3
  43. package/dist/commands/service/stop.js +41 -30
  44. package/dist/commands/wallet/list.d.ts +12 -0
  45. package/dist/commands/wallet/list.js +41 -0
  46. package/dist/constants.d.ts +2 -0
  47. package/dist/constants.js +7 -2
  48. package/dist/helper.d.ts +32 -9
  49. package/dist/helper.js +426 -49
  50. package/dist/types.d.ts +77 -0
  51. package/dist/types.js +2 -0
  52. package/dist/ui.d.ts +23 -0
  53. package/dist/ui.js +70 -0
  54. package/oclif.manifest.json +635 -25
  55. package/package.json +39 -35
package/dist/helper.js CHANGED
@@ -1,17 +1,58 @@
1
- import * as fs from "fs";
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
2
3
  import { GLOBAL_CONF_PATH } from "./constants.js";
3
- import axios from "axios";
4
- import { dirname, join, relative } from "path";
4
+ import axios, { isAxiosError } from "axios";
5
+ import { dirname, join, relative } from "node:path";
5
6
  import boxen from 'boxen';
6
7
  import chalk from 'chalk';
7
8
  import semver from 'semver';
8
- import pkgJson from 'package-json';
9
- import semverDiff from 'semver-diff';
9
+ // The config file holds plain-text API tokens, so it must stay owner-only.
10
+ const CONFIG_FILE_MODE = 0o600;
11
+ const PACKAGE_NAME = "@chabokan.net/cli";
12
+ // The update check runs before every command, so it must never be something the
13
+ // user waits on. It is capped hard, cached, and skippable.
14
+ const UPDATE_CHECK_TIMEOUT_MS = 1500;
15
+ const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
16
+ const UPDATE_CACHE_PATH = join(os.tmpdir(), 'chabok-cli', 'update-check.json');
17
+ // Node surfaces certificate problems as error codes. These are permanent
18
+ // failures: retrying will not help, and they must not be reported as a
19
+ // generic "check your internet connection".
20
+ const TLS_ERROR_CODES = new Set([
21
+ 'CERT_HAS_EXPIRED',
22
+ 'DEPTH_ZERO_SELF_SIGNED_CERT',
23
+ 'ERR_TLS_CERT_ALTNAME_INVALID',
24
+ 'SELF_SIGNED_CERT_IN_CHAIN',
25
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
26
+ ]);
27
+ const HTML_ENTITIES = {
28
+ ' ': ' ',
29
+ '‌': '‌',
30
+ '&': '&',
31
+ '&lt;': '<',
32
+ '&gt;': '>',
33
+ '&quot;': '"',
34
+ '&#39;': "'",
35
+ };
36
+ /** Some API error/description text comes wrapped in HTML tags (e.g. `<p>...</p>`). */
37
+ export function stripHtml(text) {
38
+ return text
39
+ .replaceAll(/<[^>]*>/g, '')
40
+ .replaceAll(/&[a-z#0-9]+;/gi, (entity) => HTML_ENTITIES[entity] ?? entity)
41
+ .trim();
42
+ }
43
+ function isTlsError(code) {
44
+ return code !== undefined && TLS_ERROR_CODES.has(code);
45
+ }
46
+ export function isDebug() {
47
+ return process.env.CHABOK_DEBUG === "true";
48
+ }
10
49
  export function isObject(obj) {
11
- return obj != null && obj.constructor.name === "Object";
50
+ // `typeof null === 'object'`, so null needs its own check; undefined is
51
+ // already excluded by the typeof test.
52
+ return obj !== null && typeof obj === 'object' && obj.constructor?.name === "Object";
12
53
  }
13
54
  export function isEmptyObject(obj) {
14
- return obj && obj.constructor === Object && Object.keys(obj).length === 0;
55
+ return obj !== null && typeof obj === 'object' && obj.constructor === Object && Object.keys(obj).length === 0;
15
56
  }
16
57
  export function read_config_file() {
17
58
  let config_json = {
@@ -19,38 +60,116 @@ export function read_config_file() {
19
60
  default_user: ""
20
61
  };
21
62
  try {
22
- config_json = JSON.parse(fs.readFileSync(GLOBAL_CONF_PATH).toString('utf-8')) || {};
63
+ if (!fs.existsSync(GLOBAL_CONF_PATH)) {
64
+ return config_json;
65
+ }
66
+ const fileContent = fs.readFileSync(GLOBAL_CONF_PATH).toString('utf8');
67
+ if (!fileContent.trim()) {
68
+ return config_json;
69
+ }
70
+ const parsed = JSON.parse(fileContent);
71
+ if (isObject(parsed) && 'users' in parsed && 'default_user' in parsed) {
72
+ config_json = {
73
+ users: parsed.users || {},
74
+ default_user: String(parsed.default_user || "")
75
+ };
76
+ }
23
77
  }
24
- catch {
78
+ catch (error) {
79
+ if (isDebug()) {
80
+ console.error(`Error reading config file (${GLOBAL_CONF_PATH}):`, error);
81
+ }
82
+ // File doesn't exist or invalid JSON, return default
25
83
  }
26
84
  return config_json;
27
85
  }
28
- export async function get_all_services(filter = {}, axiosConfig) {
29
- let all_services = [];
86
+ export function write_config_file(config_json) {
87
+ // `mode` only applies when the file is created, so tighten it explicitly
88
+ // afterwards for configs written by older versions of the CLI.
89
+ fs.writeFileSync(GLOBAL_CONF_PATH, JSON.stringify(config_json, null, 2), {
90
+ mode: CONFIG_FILE_MODE,
91
+ });
30
92
  try {
31
- let url_filter = "";
32
- 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}`;
93
+ fs.chmodSync(GLOBAL_CONF_PATH, CONFIG_FILE_MODE);
94
+ }
95
+ catch (error) {
96
+ // chmod is unsupported on some filesystems (e.g. Windows); not fatal.
97
+ if (isDebug()) {
98
+ console.error(`Could not restrict permissions on ${GLOBAL_CONF_PATH}:`, error);
99
+ }
100
+ }
101
+ }
102
+ /**
103
+ * Retries a request that failed before the server produced a response.
104
+ * Only safe for idempotent (GET) calls: a response-less failure still might
105
+ * mean a non-idempotent request was applied server-side.
106
+ */
107
+ async function getWithRetry(endpoint, axiosConfig, retries = 2) {
108
+ let lastError;
109
+ // Attempts are inherently sequential: each one waits for the previous
110
+ // failure and its backoff, so awaiting inside the loop is the point.
111
+ /* eslint-disable no-await-in-loop */
112
+ for (let attempt = 0; attempt <= retries; attempt++) {
113
+ try {
114
+ const { data } = await axios.get(endpoint, axiosConfig);
115
+ return data;
116
+ }
117
+ catch (error) {
118
+ lastError = error;
119
+ const isTransient = isAxiosError(error) && !error.response && !isTlsError(error.code);
120
+ if (!isTransient || attempt === retries) {
121
+ throw error;
38
122
  }
39
- else {
40
- url_filter += `&${key}=${value}`;
123
+ if (isDebug()) {
124
+ console.error(`Request to ${endpoint} failed (${error.code}), retrying...`);
41
125
  }
42
- });
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
- });
126
+ await new Promise(resolve => {
127
+ setTimeout(resolve, 500 * (attempt + 1));
128
+ });
129
+ }
48
130
  }
49
- catch (e) {
50
- console.log(e.response.data);
131
+ /* eslint-enable no-await-in-loop */
132
+ throw lastError;
133
+ }
134
+ /**
135
+ * Fetches the account's services. Throws on failure so callers can surface a
136
+ * real error instead of an empty list that reads as "you have no services".
137
+ */
138
+ export async function get_all_services(filter = {}, axiosConfig) {
139
+ const all_services = [];
140
+ const query = new URLSearchParams(filter).toString();
141
+ const endpoint = `services/${query ? `?${query}` : ""}`;
142
+ const data = await getWithRetry(endpoint, axiosConfig);
143
+ if (data.services && Array.isArray(data.services)) {
144
+ data.services.forEach((item) => {
145
+ all_services.push({
146
+ value: item.main_name,
147
+ name: `${item.main_name} (${item.platform.name})`,
148
+ platform: item.platform.name
149
+ });
150
+ });
51
151
  }
52
152
  return all_services;
53
153
  }
154
+ /**
155
+ * Pulls a list of records out of an API response whose exact wrapper key
156
+ * isn't guaranteed (some endpoints return a bare array, others wrap it under
157
+ * `data`, `results`, or a resource-named key).
158
+ */
159
+ export function extractRecords(data, wrapperKeys = []) {
160
+ if (Array.isArray(data)) {
161
+ return data.filter((item) => isObject(item));
162
+ }
163
+ if (isObject(data)) {
164
+ for (const key of wrapperKeys) {
165
+ const wrapped = data[key];
166
+ if (Array.isArray(wrapped)) {
167
+ return wrapped.filter((item) => isObject(item));
168
+ }
169
+ }
170
+ }
171
+ return [];
172
+ }
54
173
  export function trimLines(lines) {
55
174
  return lines.reduce((prev, line) => {
56
175
  if (!line.trim() || line.startsWith('#')) {
@@ -64,13 +183,13 @@ export const loadIgnoreFile = (ignoreInstance, ignoreFilePath, projectPath) => {
64
183
  const relativeToProjectPath = patterns.map((pattern) => {
65
184
  const dir = dirname(ignoreFilePath);
66
185
  if (pattern.startsWith('!')) {
67
- const absolutePrefix = pattern.substr(1).startsWith('/') ? '/' : '';
68
- return '!' + absolutePrefix + relative(projectPath, join(dir, pattern.substr(1)));
186
+ const absolutePrefix = pattern.slice(1).startsWith('/') ? '/' : '';
187
+ return '!' + absolutePrefix + relative(projectPath, join(dir, pattern.slice(1)));
69
188
  }
70
189
  const absolutePrefix = pattern.startsWith('/') ? '/' : '';
71
190
  return absolutePrefix + relative(projectPath, join(dir, pattern));
72
191
  });
73
- const linuxify = relativeToProjectPath.map(p => p.replace(/\\/g, '/'));
192
+ const linuxify = relativeToProjectPath.map(p => p.replaceAll('\\', '/'));
74
193
  ignoreInstance.add(linuxify);
75
194
  };
76
195
  export function addIgnorePatterns(ignoreInstance, projectPath, dir) {
@@ -87,27 +206,285 @@ export function addIgnorePatterns(ignoreInstance, projectPath, dir) {
87
206
  loadIgnoreFile(ignoreInstance, gitignorePath, projectPath);
88
207
  }
89
208
  }
209
+ /** True when the user has opted out of update checks, or we are in CI. */
210
+ function updateCheckDisabled() {
211
+ return Boolean(process.env.CHABOK_NO_UPDATE_CHECK ||
212
+ process.env.NO_UPDATE_NOTIFIER ||
213
+ process.env.CI);
214
+ }
215
+ function readUpdateCache() {
216
+ try {
217
+ const raw = fs.readFileSync(UPDATE_CACHE_PATH).toString('utf8');
218
+ const parsed = JSON.parse(raw);
219
+ if (typeof parsed?.checkedAt === 'number') {
220
+ return parsed;
221
+ }
222
+ }
223
+ catch {
224
+ // No cache yet, or it is unreadable — treat as "never checked".
225
+ }
226
+ return undefined;
227
+ }
228
+ function writeUpdateCache(cache) {
229
+ try {
230
+ fs.mkdirSync(dirname(UPDATE_CACHE_PATH), { recursive: true });
231
+ fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache));
232
+ }
233
+ catch (error) {
234
+ // A cache we cannot write just means we check again next time.
235
+ if (isDebug()) {
236
+ console.error('Could not write the update-check cache:', error);
237
+ }
238
+ }
239
+ }
240
+ /**
241
+ * Asks the npm registry for the latest published version, giving up after
242
+ * `timeoutMs`. axios aborts on timeout rather than leaving the socket
243
+ * dangling, so a slow network cannot hold the process open after the command
244
+ * has finished.
245
+ */
246
+ async function fetchLatestVersion(timeoutMs) {
247
+ const registry = (process.env.CHABOK_NPM_REGISTRY || 'https://registry.npmjs.org').replace(/\/+$/, '');
248
+ const url = `${registry}/${PACKAGE_NAME.replace('/', '%2f')}/latest`;
249
+ const { data } = await axios.get(url, {
250
+ timeout: timeoutMs,
251
+ headers: { accept: 'application/json' },
252
+ // This call is deliberately independent of the CLI's API client: no auth
253
+ // header, no baseURL, no proxy-agent inherited from the command.
254
+ transitional: { clarifyTimeoutError: true },
255
+ });
256
+ return data.version;
257
+ }
258
+ /**
259
+ * Tells the user about a newer release. This runs ahead of every command, so it
260
+ * is deliberately defensive: it is skippable, it only hits the network once a
261
+ * day, it gives up after 1.5s, and any failure is silent. A user with no
262
+ * internet should not notice that this function exists.
263
+ */
90
264
  export const checkUpdate = async (version) => {
91
- 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}`, {
265
+ if (updateCheckDisabled()) {
266
+ return;
267
+ }
268
+ try {
269
+ const cache = readUpdateCache();
270
+ const cacheIsFresh = cache && Date.now() - cache.checkedAt < UPDATE_CHECK_INTERVAL_MS;
271
+ let latestVersion = cacheIsFresh ? cache.latestVersion : undefined;
272
+ if (!cacheIsFresh) {
273
+ try {
274
+ latestVersion = await fetchLatestVersion(UPDATE_CHECK_TIMEOUT_MS);
275
+ }
276
+ catch (error) {
277
+ // A failed lookup must still be recorded below, otherwise every command
278
+ // run offline pays the timeout again.
279
+ if (isDebug()) {
280
+ console.error('Update check request failed:', error);
281
+ }
282
+ }
283
+ writeUpdateCache({ checkedAt: Date.now(), latestVersion });
284
+ }
285
+ if (!latestVersion || !semver.valid(latestVersion) || !semver.lt(version, latestVersion)) {
286
+ return;
287
+ }
288
+ const updateType = semver.diff(version, latestVersion) ?? '';
289
+ const lines = [
290
+ `${updateType} update available ${chalk.dim(version)} → ${chalk.green(latestVersion)}`,
291
+ `Run ${chalk.cyan(`npm i -g ${PACKAGE_NAME}`)} to update`,
292
+ ];
293
+ // stderr, not stdout: the banner must never end up inside piped output
294
+ // such as `chabok service logs -s app > app.log`.
295
+ process.stderr.write(boxen(lines.join('\n'), {
108
296
  margin: 1,
109
297
  padding: 1,
110
298
  align: 'center',
111
- }));
299
+ }) + '\n');
300
+ }
301
+ catch (error) {
302
+ // Never let the update check break, delay or noisily fail a real command.
303
+ if (isDebug()) {
304
+ console.error('Failed to check for updates:', error);
305
+ }
112
306
  }
113
307
  };
308
+ export function handleApiError(error, defaultMessage, context) {
309
+ const errorInfo = {
310
+ message: defaultMessage,
311
+ };
312
+ if (context?.endpoint) {
313
+ errorInfo.endpoint = context.endpoint;
314
+ }
315
+ if (context?.serviceName) {
316
+ errorInfo.serviceName = context.serviceName;
317
+ }
318
+ if (context?.operation) {
319
+ errorInfo.operation = context.operation;
320
+ }
321
+ if (isAxiosError(error)) {
322
+ // Extract endpoint from error config
323
+ if (error.config?.url) {
324
+ errorInfo.endpoint = error.config.url;
325
+ }
326
+ if (error.config?.baseURL) {
327
+ errorInfo.endpoint = `${error.config.baseURL}${errorInfo.endpoint || ''}`;
328
+ }
329
+ if (error.response) {
330
+ const { status } = error.response;
331
+ const { data } = error.response;
332
+ errorInfo.status = status;
333
+ // Build detailed error message based on status code
334
+ let message = defaultMessage;
335
+ switch (status) {
336
+ case 400:
337
+ message = "Bad Request: Invalid parameters provided.";
338
+ break;
339
+ case 401:
340
+ message = "Authentication failed: Please login again using 'chabok login'.";
341
+ break;
342
+ case 403:
343
+ message = "Access denied: You don't have permission to perform this action.";
344
+ break;
345
+ case 404:
346
+ if (context?.serviceName) {
347
+ message = `Service '${context.serviceName}' not found. Please check the service name and try again.`;
348
+ }
349
+ else if (context?.operation) {
350
+ message = `${context.operation} not found. Please verify the information and try again.`;
351
+ }
352
+ else {
353
+ message = "Resource not found. Please verify the information and try again.";
354
+ }
355
+ break;
356
+ case 409:
357
+ message = "Conflict: The resource already exists or is in an invalid state.";
358
+ break;
359
+ case 422:
360
+ message = "Validation error: The provided data is invalid.";
361
+ break;
362
+ case 429:
363
+ message = "Rate limit exceeded: Too many requests. Please try again later.";
364
+ break;
365
+ case 500:
366
+ message = "Server error: The server encountered an internal error. Please try again later.";
367
+ break;
368
+ case 502:
369
+ message = "Bad Gateway: The server is temporarily unavailable. Please try again later.";
370
+ break;
371
+ case 503:
372
+ message = "Service unavailable: The service is temporarily down. Please try again later.";
373
+ break;
374
+ default:
375
+ message = defaultMessage;
376
+ }
377
+ // Try to extract detailed error message from response. Some endpoints
378
+ // (e.g. cloudservers/create/) reply with a `messages` (plural) key
379
+ // instead of `message`, and wrap the text in HTML tags.
380
+ if (typeof data === 'object' && data !== null) {
381
+ if ('message' in data && typeof data.message === 'string') {
382
+ errorInfo.details = stripHtml(data.message);
383
+ message = `${message} ${errorInfo.details}`;
384
+ }
385
+ else if ('messages' in data && typeof data.messages === 'string') {
386
+ errorInfo.details = stripHtml(data.messages);
387
+ message = `${message} ${errorInfo.details}`;
388
+ }
389
+ else if ('messages' in data && Array.isArray(data.messages)) {
390
+ errorInfo.details = data.messages.map((m) => stripHtml(String(m))).join(' ');
391
+ message = `${message} ${errorInfo.details}`;
392
+ }
393
+ else if ('error' in data && typeof data.error === 'string') {
394
+ errorInfo.details = stripHtml(data.error);
395
+ message = `${message} ${errorInfo.details}`;
396
+ }
397
+ else if ('detail' in data && typeof data.detail === 'string') {
398
+ errorInfo.details = stripHtml(data.detail);
399
+ message = `${message} ${errorInfo.details}`;
400
+ }
401
+ else if (Array.isArray(data) && data.length > 0) {
402
+ errorInfo.details = JSON.stringify(data);
403
+ message = `${message} ${JSON.stringify(data)}`;
404
+ }
405
+ else if (Object.keys(data).length > 0) {
406
+ errorInfo.details = JSON.stringify(data);
407
+ }
408
+ }
409
+ else if (typeof data === 'string') {
410
+ errorInfo.details = stripHtml(data);
411
+ message = `${message} ${errorInfo.details}`;
412
+ }
413
+ errorInfo.message = message;
414
+ errorInfo.code = error.code || `HTTP_${status}`;
415
+ }
416
+ else if (error.request) {
417
+ // Request was made but no response received
418
+ if (isTlsError(error.code)) {
419
+ errorInfo.message = `TLS error (${error.code}): the server's certificate could not be verified. If you are intentionally targeting a server with a self-signed certificate, set CHABOK_INSECURE_TLS=true.`;
420
+ errorInfo.code = error.code;
421
+ }
422
+ else {
423
+ switch (error.code) {
424
+ case 'ECONNABORTED':
425
+ case 'ETIMEDOUT': {
426
+ errorInfo.message = "Request timeout: The server took too long to respond. Please check your connection and try again.";
427
+ errorInfo.code = error.code;
428
+ break;
429
+ }
430
+ case 'ECONNREFUSED': {
431
+ errorInfo.message = "Connection refused: Unable to connect to the server. Please check your internet connection and try again.";
432
+ errorInfo.code = 'ECONNREFUSED';
433
+ break;
434
+ }
435
+ case 'ENOTFOUND': {
436
+ errorInfo.message = "DNS error: Unable to resolve server address. Please check your internet connection.";
437
+ errorInfo.code = 'ENOTFOUND';
438
+ break;
439
+ }
440
+ default: {
441
+ errorInfo.message = "Network error: Unable to reach the server. Please check your internet connection and try again.";
442
+ errorInfo.code = error.code || 'NETWORK_ERROR';
443
+ }
444
+ }
445
+ }
446
+ }
447
+ else {
448
+ // Error in request setup
449
+ errorInfo.message = `Request setup error: ${error.message || defaultMessage}`;
450
+ errorInfo.code = error.code || 'REQUEST_ERROR';
451
+ }
452
+ }
453
+ else if (error instanceof Error) {
454
+ errorInfo.message = error.message || defaultMessage;
455
+ errorInfo.code = error.code || 'UNKNOWN_ERROR';
456
+ errorInfo.details = error.stack;
457
+ }
458
+ else if (typeof error === 'string') {
459
+ errorInfo.message = error;
460
+ }
461
+ // Context is deliberately not prefixed onto the message: callers already
462
+ // mention the service they are acting on, and the status-specific messages
463
+ // above name it too. It is kept on `errorInfo` for the debug output.
464
+ return errorInfo;
465
+ }
466
+ export function logErrorDetails(errorInfo, command) {
467
+ if (!isDebug()) {
468
+ return;
469
+ }
470
+ command.log(chalk.dim('--- Error Details ---'));
471
+ if (errorInfo.operation) {
472
+ command.log(chalk.dim(`Operation: ${errorInfo.operation}`));
473
+ }
474
+ if (errorInfo.serviceName) {
475
+ command.log(chalk.dim(`Service: ${errorInfo.serviceName}`));
476
+ }
477
+ if (errorInfo.status) {
478
+ command.log(chalk.dim(`Status Code: ${errorInfo.status}`));
479
+ }
480
+ if (errorInfo.endpoint) {
481
+ command.log(chalk.dim(`Endpoint: ${errorInfo.endpoint}`));
482
+ }
483
+ if (errorInfo.code) {
484
+ command.log(chalk.dim(`Error Code: ${errorInfo.code}`));
485
+ }
486
+ if (errorInfo.details) {
487
+ command.log(chalk.dim(`Details: ${errorInfo.details}`));
488
+ }
489
+ command.log(chalk.dim('--- End Error Details ---'));
490
+ }
@@ -0,0 +1,77 @@
1
+ export interface ConfigFile {
2
+ users: Record<string, UserConfig>;
3
+ default_user: string;
4
+ }
5
+ export interface UserConfig {
6
+ token: string;
7
+ }
8
+ /**
9
+ * A service as offered to the user. `value`/`name` are the shape inquirer
10
+ * expects for a choice; `platform` is kept separately so tables do not have to
11
+ * parse it back out of the label.
12
+ */
13
+ export interface Service {
14
+ value: string;
15
+ name: string;
16
+ platform: string;
17
+ }
18
+ export interface ApiResponse<T = unknown> {
19
+ success: boolean;
20
+ data?: T;
21
+ message?: string;
22
+ [key: string]: unknown;
23
+ }
24
+ export interface LoginResponse {
25
+ success: boolean;
26
+ token?: string;
27
+ }
28
+ export interface UserInfo {
29
+ user: {
30
+ email: string;
31
+ [key: string]: unknown;
32
+ };
33
+ }
34
+ export interface ServiceData {
35
+ main_name: string;
36
+ platform: {
37
+ name: string;
38
+ };
39
+ [key: string]: unknown;
40
+ }
41
+ export interface ServicesResponse {
42
+ services: ServiceData[];
43
+ }
44
+ export interface ChabokFile {
45
+ service?: string;
46
+ [key: string]: unknown;
47
+ }
48
+ export interface ErrorContext {
49
+ endpoint?: string;
50
+ serviceName?: string;
51
+ operation?: string;
52
+ }
53
+ export interface ErrorInfo {
54
+ message: string;
55
+ status?: number;
56
+ endpoint?: string;
57
+ details?: string;
58
+ code?: string;
59
+ serviceName?: string;
60
+ operation?: string;
61
+ }
62
+ export interface DomainResponse extends ApiResponse {
63
+ domain_name?: string;
64
+ }
65
+ /**
66
+ * A resolved cloud server. `id` is what the API needs in request bodies;
67
+ * `hostname` is the only part ever shown to the user.
68
+ */
69
+ export interface CloudServerRef {
70
+ id: string;
71
+ hostname: string;
72
+ }
73
+ /** A resolved wallet. `id` is what the API needs; `name` is shown to the user. */
74
+ export interface WalletRef {
75
+ id: string;
76
+ name: string;
77
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // Type definitions for the CLI
2
+ export {};
package/dist/ui.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ export declare function success(message: string): string;
2
+ export declare function error(message: string): string;
3
+ export declare function warning(message: string): string;
4
+ export declare function info(message: string): string;
5
+ /** A dimmed follow-up line telling the user what to do next. */
6
+ export declare function hint(message: string): string;
7
+ /** Highlights a command the user is meant to run. */
8
+ export declare function cmd(command: string): string;
9
+ /** Highlights a value the user supplied or chose, such as a service name. */
10
+ export declare function value(text: string): string;
11
+ /**
12
+ * Renders an array of API records as a table without assuming which fields
13
+ * it contains — the columns are whichever keys are present on the first row.
14
+ * Used for endpoints whose response shape isn't fixed enough to hard-code a
15
+ * column list against.
16
+ */
17
+ export declare function printRecords(records: Record<string, unknown>[]): void;
18
+ /**
19
+ * Renders a single API record as Field/Value rows. Preferred over
20
+ * `printRecords` for one object with many fields, which would otherwise
21
+ * produce one very wide, mostly-empty table.
22
+ */
23
+ export declare function printRecord(record: Record<string, unknown>): void;