@chabokan.net/cli 0.8.15 → 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.
- package/README.md +520 -66
- package/dist/base.d.ts +30 -7
- package/dist/base.js +163 -14
- package/dist/commands/account/info.d.ts +12 -0
- package/dist/commands/account/info.js +40 -0
- package/dist/commands/account/list.d.ts +5 -1
- package/dist/commands/account/list.js +29 -19
- package/dist/commands/account/remove.d.ts +5 -1
- package/dist/commands/account/remove.js +23 -12
- package/dist/commands/account/use.d.ts +6 -2
- package/dist/commands/account/use.js +22 -11
- package/dist/commands/cloudserver/create.d.ts +40 -0
- package/dist/commands/cloudserver/create.js +242 -0
- package/dist/commands/cloudserver/delete.d.ts +14 -0
- package/dist/commands/cloudserver/delete.js +65 -0
- package/dist/commands/cloudserver/list.d.ts +12 -0
- package/dist/commands/cloudserver/list.js +46 -0
- package/dist/commands/cloudserver/restart.d.ts +13 -0
- package/dist/commands/cloudserver/restart.js +51 -0
- package/dist/commands/cloudserver/start.d.ts +13 -0
- package/dist/commands/cloudserver/start.js +51 -0
- package/dist/commands/cloudserver/stop.d.ts +13 -0
- package/dist/commands/cloudserver/stop.js +52 -0
- package/dist/commands/deploy.d.ts +13 -3
- package/dist/commands/deploy.js +96 -60
- package/dist/commands/login.d.ts +8 -4
- package/dist/commands/login.js +69 -41
- package/dist/commands/service/domain/add.d.ts +15 -0
- package/dist/commands/service/domain/add.js +74 -0
- package/dist/commands/service/domain/remove.d.ts +15 -0
- package/dist/commands/service/domain/remove.js +72 -0
- package/dist/commands/service/list.d.ts +5 -1
- package/dist/commands/service/list.js +29 -21
- package/dist/commands/service/logs.d.ts +6 -2
- package/dist/commands/service/logs.js +33 -30
- package/dist/commands/service/resize.d.ts +9 -5
- package/dist/commands/service/resize.js +73 -69
- package/dist/commands/service/restart.d.ts +6 -2
- package/dist/commands/service/restart.js +28 -32
- package/dist/commands/service/start.d.ts +6 -2
- package/dist/commands/service/start.js +29 -31
- package/dist/commands/service/stop.d.ts +6 -2
- package/dist/commands/service/stop.js +29 -31
- package/dist/commands/wallet/list.d.ts +12 -0
- package/dist/commands/wallet/list.js +41 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +7 -2
- package/dist/helper.d.ts +23 -7
- package/dist/helper.js +294 -113
- package/dist/types.d.ts +28 -8
- package/dist/ui.d.ts +23 -0
- package/dist/ui.js +70 -0
- package/oclif.manifest.json +635 -25
- package/package.json +39 -35
package/dist/helper.js
CHANGED
|
@@ -1,14 +1,55 @@
|
|
|
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
|
-
|
|
9
|
-
|
|
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
|
+
'<': '<',
|
|
32
|
+
'>': '>',
|
|
33
|
+
'"': '"',
|
|
34
|
+
''': "'",
|
|
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
|
-
|
|
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
55
|
return obj !== null && typeof obj === 'object' && obj.constructor === Object && Object.keys(obj).length === 0;
|
|
@@ -22,7 +63,7 @@ export function read_config_file() {
|
|
|
22
63
|
if (!fs.existsSync(GLOBAL_CONF_PATH)) {
|
|
23
64
|
return config_json;
|
|
24
65
|
}
|
|
25
|
-
const fileContent = fs.readFileSync(GLOBAL_CONF_PATH).toString('
|
|
66
|
+
const fileContent = fs.readFileSync(GLOBAL_CONF_PATH).toString('utf8');
|
|
26
67
|
if (!fileContent.trim()) {
|
|
27
68
|
return config_json;
|
|
28
69
|
}
|
|
@@ -35,57 +76,99 @@ export function read_config_file() {
|
|
|
35
76
|
}
|
|
36
77
|
}
|
|
37
78
|
catch (error) {
|
|
38
|
-
if (
|
|
79
|
+
if (isDebug()) {
|
|
39
80
|
console.error(`Error reading config file (${GLOBAL_CONF_PATH}):`, error);
|
|
40
81
|
}
|
|
41
82
|
// File doesn't exist or invalid JSON, return default
|
|
42
83
|
}
|
|
43
84
|
return config_json;
|
|
44
85
|
}
|
|
45
|
-
export
|
|
46
|
-
|
|
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
|
+
});
|
|
47
92
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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;
|
|
54
122
|
}
|
|
55
|
-
|
|
56
|
-
|
|
123
|
+
if (isDebug()) {
|
|
124
|
+
console.error(`Request to ${endpoint} failed (${error.code}), retrying...`);
|
|
57
125
|
}
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
});
|
|
126
|
+
await new Promise(resolve => {
|
|
127
|
+
setTimeout(resolve, 500 * (attempt + 1));
|
|
67
128
|
});
|
|
68
129
|
}
|
|
69
130
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
});
|
|
74
150
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
151
|
+
}
|
|
152
|
+
return all_services;
|
|
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));
|
|
85
168
|
}
|
|
86
169
|
}
|
|
87
170
|
}
|
|
88
|
-
return
|
|
171
|
+
return [];
|
|
89
172
|
}
|
|
90
173
|
export function trimLines(lines) {
|
|
91
174
|
return lines.reduce((prev, line) => {
|
|
@@ -100,13 +183,13 @@ export const loadIgnoreFile = (ignoreInstance, ignoreFilePath, projectPath) => {
|
|
|
100
183
|
const relativeToProjectPath = patterns.map((pattern) => {
|
|
101
184
|
const dir = dirname(ignoreFilePath);
|
|
102
185
|
if (pattern.startsWith('!')) {
|
|
103
|
-
const absolutePrefix = pattern.
|
|
104
|
-
return '!' + absolutePrefix + relative(projectPath, join(dir, pattern.
|
|
186
|
+
const absolutePrefix = pattern.slice(1).startsWith('/') ? '/' : '';
|
|
187
|
+
return '!' + absolutePrefix + relative(projectPath, join(dir, pattern.slice(1)));
|
|
105
188
|
}
|
|
106
189
|
const absolutePrefix = pattern.startsWith('/') ? '/' : '';
|
|
107
190
|
return absolutePrefix + relative(projectPath, join(dir, pattern));
|
|
108
191
|
});
|
|
109
|
-
const linuxify = relativeToProjectPath.map(p => p.
|
|
192
|
+
const linuxify = relativeToProjectPath.map(p => p.replaceAll('\\', '/'));
|
|
110
193
|
ignoreInstance.add(linuxify);
|
|
111
194
|
};
|
|
112
195
|
export function addIgnorePatterns(ignoreInstance, projectPath, dir) {
|
|
@@ -123,34 +206,101 @@ export function addIgnorePatterns(ignoreInstance, projectPath, dir) {
|
|
|
123
206
|
loadIgnoreFile(ignoreInstance, gitignorePath, projectPath);
|
|
124
207
|
}
|
|
125
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
|
+
*/
|
|
126
264
|
export const checkUpdate = async (version) => {
|
|
127
|
-
|
|
265
|
+
if (updateCheckDisabled()) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
128
268
|
try {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const verDiff = semverDiff(version, latestVersion);
|
|
136
|
-
if (verDiff) {
|
|
137
|
-
updateType = verDiff;
|
|
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);
|
|
138
275
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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;
|
|
149
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'), {
|
|
296
|
+
margin: 1,
|
|
297
|
+
padding: 1,
|
|
298
|
+
align: 'center',
|
|
299
|
+
}) + '\n');
|
|
150
300
|
}
|
|
151
301
|
catch (error) {
|
|
152
|
-
//
|
|
153
|
-
if (
|
|
302
|
+
// Never let the update check break, delay or noisily fail a real command.
|
|
303
|
+
if (isDebug()) {
|
|
154
304
|
console.error('Failed to check for updates:', error);
|
|
155
305
|
}
|
|
156
306
|
}
|
|
@@ -162,7 +312,13 @@ export function handleApiError(error, defaultMessage, context) {
|
|
|
162
312
|
if (context?.endpoint) {
|
|
163
313
|
errorInfo.endpoint = context.endpoint;
|
|
164
314
|
}
|
|
165
|
-
if (
|
|
315
|
+
if (context?.serviceName) {
|
|
316
|
+
errorInfo.serviceName = context.serviceName;
|
|
317
|
+
}
|
|
318
|
+
if (context?.operation) {
|
|
319
|
+
errorInfo.operation = context.operation;
|
|
320
|
+
}
|
|
321
|
+
if (isAxiosError(error)) {
|
|
166
322
|
// Extract endpoint from error config
|
|
167
323
|
if (error.config?.url) {
|
|
168
324
|
errorInfo.endpoint = error.config.url;
|
|
@@ -171,8 +327,8 @@ export function handleApiError(error, defaultMessage, context) {
|
|
|
171
327
|
errorInfo.endpoint = `${error.config.baseURL}${errorInfo.endpoint || ''}`;
|
|
172
328
|
}
|
|
173
329
|
if (error.response) {
|
|
174
|
-
const status = error.response
|
|
175
|
-
const data = error.response
|
|
330
|
+
const { status } = error.response;
|
|
331
|
+
const { data } = error.response;
|
|
176
332
|
errorInfo.status = status;
|
|
177
333
|
// Build detailed error message based on status code
|
|
178
334
|
let message = defaultMessage;
|
|
@@ -218,19 +374,29 @@ export function handleApiError(error, defaultMessage, context) {
|
|
|
218
374
|
default:
|
|
219
375
|
message = defaultMessage;
|
|
220
376
|
}
|
|
221
|
-
// Try to extract detailed error message from response
|
|
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.
|
|
222
380
|
if (typeof data === 'object' && data !== null) {
|
|
223
381
|
if ('message' in data && typeof data.message === 'string') {
|
|
224
|
-
errorInfo.details = data.message;
|
|
225
|
-
message = `${message} ${
|
|
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}`;
|
|
226
392
|
}
|
|
227
393
|
else if ('error' in data && typeof data.error === 'string') {
|
|
228
|
-
errorInfo.details = data.error;
|
|
229
|
-
message = `${message} ${
|
|
394
|
+
errorInfo.details = stripHtml(data.error);
|
|
395
|
+
message = `${message} ${errorInfo.details}`;
|
|
230
396
|
}
|
|
231
397
|
else if ('detail' in data && typeof data.detail === 'string') {
|
|
232
|
-
errorInfo.details = data.detail;
|
|
233
|
-
message = `${message} ${
|
|
398
|
+
errorInfo.details = stripHtml(data.detail);
|
|
399
|
+
message = `${message} ${errorInfo.details}`;
|
|
234
400
|
}
|
|
235
401
|
else if (Array.isArray(data) && data.length > 0) {
|
|
236
402
|
errorInfo.details = JSON.stringify(data);
|
|
@@ -241,29 +407,41 @@ export function handleApiError(error, defaultMessage, context) {
|
|
|
241
407
|
}
|
|
242
408
|
}
|
|
243
409
|
else if (typeof data === 'string') {
|
|
244
|
-
errorInfo.details = data;
|
|
245
|
-
message = `${message} ${
|
|
410
|
+
errorInfo.details = stripHtml(data);
|
|
411
|
+
message = `${message} ${errorInfo.details}`;
|
|
246
412
|
}
|
|
247
413
|
errorInfo.message = message;
|
|
248
414
|
errorInfo.code = error.code || `HTTP_${status}`;
|
|
249
415
|
}
|
|
250
416
|
else if (error.request) {
|
|
251
417
|
// Request was made but no response received
|
|
252
|
-
if (error.code
|
|
253
|
-
errorInfo.message =
|
|
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.";
|
|
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.`;
|
|
258
420
|
errorInfo.code = error.code;
|
|
259
421
|
}
|
|
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
422
|
else {
|
|
265
|
-
|
|
266
|
-
|
|
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
|
+
}
|
|
267
445
|
}
|
|
268
446
|
}
|
|
269
447
|
else {
|
|
@@ -280,30 +458,33 @@ export function handleApiError(error, defaultMessage, context) {
|
|
|
280
458
|
else if (typeof error === 'string') {
|
|
281
459
|
errorInfo.message = error;
|
|
282
460
|
}
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
if (context?.operation && !errorInfo.message.includes(context.operation)) {
|
|
288
|
-
errorInfo.message = `[Operation: ${context.operation}] ${errorInfo.message}`;
|
|
289
|
-
}
|
|
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.
|
|
290
464
|
return errorInfo;
|
|
291
465
|
}
|
|
292
466
|
export function logErrorDetails(errorInfo, command) {
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
command.log(chalk.dim(
|
|
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}`));
|
|
308
488
|
}
|
|
489
|
+
command.log(chalk.dim('--- End Error Details ---'));
|
|
309
490
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -5,9 +5,15 @@ export interface ConfigFile {
|
|
|
5
5
|
export interface UserConfig {
|
|
6
6
|
token: string;
|
|
7
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
|
+
*/
|
|
8
13
|
export interface Service {
|
|
9
14
|
value: string;
|
|
10
15
|
name: string;
|
|
16
|
+
platform: string;
|
|
11
17
|
}
|
|
12
18
|
export interface ApiResponse<T = unknown> {
|
|
13
19
|
success: boolean;
|
|
@@ -39,14 +45,10 @@ export interface ChabokFile {
|
|
|
39
45
|
service?: string;
|
|
40
46
|
[key: string]: unknown;
|
|
41
47
|
}
|
|
42
|
-
export interface
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
[key: string]: unknown;
|
|
47
|
-
};
|
|
48
|
-
data?: unknown;
|
|
49
|
-
message?: string;
|
|
48
|
+
export interface ErrorContext {
|
|
49
|
+
endpoint?: string;
|
|
50
|
+
serviceName?: string;
|
|
51
|
+
operation?: string;
|
|
50
52
|
}
|
|
51
53
|
export interface ErrorInfo {
|
|
52
54
|
message: string;
|
|
@@ -54,4 +56,22 @@ export interface ErrorInfo {
|
|
|
54
56
|
endpoint?: string;
|
|
55
57
|
details?: string;
|
|
56
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;
|
|
57
77
|
}
|
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;
|