@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.
- package/README.md +520 -66
- package/dist/base.d.ts +34 -9
- package/dist/base.js +172 -20
- 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 +35 -16
- package/dist/commands/account/remove.d.ts +5 -1
- package/dist/commands/account/remove.js +30 -15
- package/dist/commands/account/use.d.ts +6 -2
- package/dist/commands/account/use.js +26 -15
- 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 +16 -5
- package/dist/commands/deploy.js +182 -101
- package/dist/commands/login.d.ts +8 -4
- package/dist/commands/login.js +97 -49
- 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 +41 -20
- package/dist/commands/service/logs.d.ts +7 -3
- package/dist/commands/service/logs.js +46 -34
- package/dist/commands/service/resize.d.ts +10 -6
- package/dist/commands/service/resize.js +94 -72
- package/dist/commands/service/restart.d.ts +7 -3
- package/dist/commands/service/restart.js +40 -31
- package/dist/commands/service/start.d.ts +7 -3
- package/dist/commands/service/start.js +41 -30
- package/dist/commands/service/stop.d.ts +7 -3
- package/dist/commands/service/stop.js +41 -30
- 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 +32 -9
- package/dist/helper.js +426 -49
- package/dist/types.d.ts +77 -0
- package/dist/types.js +2 -0
- 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,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
|
-
|
|
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
|
-
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
|
-
|
|
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
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
123
|
+
if (isDebug()) {
|
|
124
|
+
console.error(`Request to ${endpoint} failed (${error.code}), retrying...`);
|
|
41
125
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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.
|
|
68
|
-
return '!' + absolutePrefix + relative(projectPath, join(dir, pattern.
|
|
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.
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
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;
|