@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/commands/deploy.js
CHANGED
|
@@ -1,97 +1,129 @@
|
|
|
1
1
|
import { Flags } from "@oclif/core";
|
|
2
|
-
import
|
|
3
|
-
import chalk from "chalk";
|
|
2
|
+
import * as ui from "../ui.js";
|
|
4
3
|
import Command from "../base.js";
|
|
5
|
-
import { addIgnorePatterns,
|
|
4
|
+
import { addIgnorePatterns, isDebug, handleApiError, logErrorDetails } from "../helper.js";
|
|
6
5
|
import ProgressBar from "progress";
|
|
7
6
|
import FormData from "form-data";
|
|
8
7
|
import ignore from "ignore";
|
|
9
|
-
import { dirname } from "path";
|
|
10
|
-
import * as path from "path";
|
|
11
|
-
import * as os from "os";
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import * as os from "node:os";
|
|
12
11
|
import fs from "fs-extra";
|
|
13
12
|
import * as tar from 'tar';
|
|
14
13
|
const MAX_SOURCE_SIZE = 100 * 1024 * 1024; // 100 MB
|
|
14
|
+
/** Renders a byte count the way a person would say it out loud. */
|
|
15
|
+
function formatBytes(bytes) {
|
|
16
|
+
if (bytes < 1024)
|
|
17
|
+
return `${bytes} B`;
|
|
18
|
+
const units = ["KB", "MB", "GB"];
|
|
19
|
+
let size = bytes / 1024;
|
|
20
|
+
let unit = 0;
|
|
21
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
22
|
+
size /= 1024;
|
|
23
|
+
unit++;
|
|
24
|
+
}
|
|
25
|
+
return `${size < 10 ? size.toFixed(1) : Math.round(size)} ${units[unit]}`;
|
|
26
|
+
}
|
|
15
27
|
export default class Deploy extends Command {
|
|
16
|
-
static description = "
|
|
28
|
+
static description = "upload the current project and deploy it to a service";
|
|
29
|
+
static examples = [
|
|
30
|
+
{
|
|
31
|
+
description: 'Deploy the current directory, picking the service from a list',
|
|
32
|
+
command: '<%= config.bin %> deploy',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
description: 'Deploy the current directory to a named service',
|
|
36
|
+
command: '<%= config.bin %> deploy --service my-app',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
description: 'Deploy a project that lives somewhere else',
|
|
40
|
+
command: '<%= config.bin %> deploy --path ./apps/api --service my-api',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
description: 'Pin the target service in the project, so plain `deploy` always works',
|
|
44
|
+
command: 'echo \'{"service": "my-app"}\' > chabok.json && <%= config.bin %> deploy',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
description: 'See exactly which files are excluded from the upload',
|
|
48
|
+
command: 'CHABOK_DEBUG=true <%= config.bin %> deploy -s my-app',
|
|
49
|
+
},
|
|
50
|
+
];
|
|
17
51
|
static flags = {
|
|
18
52
|
...Command.flags,
|
|
19
53
|
path: Flags.string({
|
|
20
54
|
char: "p",
|
|
21
|
-
description: "
|
|
55
|
+
description: "project directory to upload (defaults to the current directory)",
|
|
22
56
|
}),
|
|
23
|
-
service: Flags.string({ char: "s", description: "service
|
|
57
|
+
service: Flags.string({ char: "s", description: "name of the service to deploy to" }),
|
|
24
58
|
};
|
|
25
59
|
async run() {
|
|
26
60
|
const { flags } = await this.parse(Deploy);
|
|
27
61
|
const cli = this;
|
|
28
62
|
await this.init_run();
|
|
29
|
-
const config_json = await this.read_config();
|
|
30
63
|
let selected_service = flags.service;
|
|
31
64
|
const project_path = flags.path ? flags.path : process.cwd();
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const chabokFilePath = path.join(project_path, "chabok.json");
|
|
35
|
-
if (fs.existsSync(chabokFilePath)) {
|
|
36
|
-
chabok_file = JSON.parse(fs.readFileSync(chabokFilePath).toString("utf-8")) || {};
|
|
37
|
-
cli.log("Reading Config from chabok.json ...");
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
if (process.env.CHABOK_DEBUG === "true") {
|
|
42
|
-
cli.log(`Error reading chabok.json: ${error}`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
65
|
+
const chabok_file = this.read_chabok_file(project_path);
|
|
45
66
|
if (chabok_file.service) {
|
|
46
67
|
selected_service = chabok_file.service;
|
|
47
68
|
}
|
|
48
|
-
if (
|
|
49
|
-
cli.log(`${chalk.red("[Error]")} first you should login!`);
|
|
69
|
+
if (!(await this.require_login())) {
|
|
50
70
|
return;
|
|
51
71
|
}
|
|
72
|
+
selected_service = await this.select_service(selected_service, { has_deploy: "true", status: "on" }, "Deploy");
|
|
52
73
|
if (!selected_service) {
|
|
53
|
-
const all_services = await this.get_services({
|
|
54
|
-
has_deploy: "true",
|
|
55
|
-
status: "on",
|
|
56
|
-
});
|
|
57
|
-
if (all_services.length > 0) {
|
|
58
|
-
const { service } = await inquirer.prompt({
|
|
59
|
-
type: "list",
|
|
60
|
-
message: "Please select a service:",
|
|
61
|
-
name: "service",
|
|
62
|
-
choices: all_services,
|
|
63
|
-
});
|
|
64
|
-
selected_service = service;
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
cli.log("first you should create service");
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (!selected_service) {
|
|
72
|
-
cli.log(`${chalk.red("[Error]")} No service selected.`);
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
76
|
try {
|
|
76
77
|
const archive_path = await this.prepare_archive(project_path);
|
|
77
78
|
const upload_response = await this.upload(archive_path, selected_service, cli, chabok_file);
|
|
78
79
|
if (upload_response.success) {
|
|
79
|
-
cli.log(
|
|
80
|
+
cli.log(ui.success(upload_response.message));
|
|
81
|
+
cli.log(ui.hint(`Watch it come up with ${ui.cmd(`chabok service logs -s ${selected_service}`)}`));
|
|
80
82
|
}
|
|
81
83
|
else {
|
|
82
|
-
cli.log(
|
|
84
|
+
cli.log(ui.error(upload_response.message));
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
catch (error) {
|
|
86
|
-
const errorInfo = handleApiError(error, 'Deployment failed.
|
|
88
|
+
const errorInfo = handleApiError(error, 'Deployment failed. Check your project files and try again.', {
|
|
87
89
|
endpoint: `services/${selected_service}/deploy/`,
|
|
88
90
|
serviceName: selected_service,
|
|
89
91
|
operation: 'Deploy'
|
|
90
92
|
});
|
|
91
|
-
cli.log(
|
|
93
|
+
cli.log(ui.error(errorInfo.message));
|
|
92
94
|
logErrorDetails(errorInfo, cli);
|
|
93
95
|
}
|
|
94
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Loads chabok.json from the project root. A missing file is normal; a
|
|
99
|
+
* malformed one is reported rather than silently ignored, because deploying
|
|
100
|
+
* with the wrong options is worse than not deploying.
|
|
101
|
+
*/
|
|
102
|
+
read_chabok_file(project_path) {
|
|
103
|
+
const chabokFilePath = path.join(project_path, "chabok.json");
|
|
104
|
+
if (!fs.existsSync(chabokFilePath)) {
|
|
105
|
+
return { service: "" };
|
|
106
|
+
}
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(fs.readFileSync(chabokFilePath).toString("utf8"));
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.warn(`Ignoring chabok.json: it is not valid JSON. ${error instanceof Error ? error.message : ""}`);
|
|
113
|
+
return { service: "" };
|
|
114
|
+
}
|
|
115
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
116
|
+
this.warn("Ignoring chabok.json: expected a JSON object.");
|
|
117
|
+
return { service: "" };
|
|
118
|
+
}
|
|
119
|
+
const chabok_file = parsed;
|
|
120
|
+
if (chabok_file.service !== undefined && typeof chabok_file.service !== "string") {
|
|
121
|
+
this.warn('Ignoring "service" in chabok.json: expected a string.');
|
|
122
|
+
delete chabok_file.service;
|
|
123
|
+
}
|
|
124
|
+
this.log(ui.info('Using settings from chabok.json'));
|
|
125
|
+
return chabok_file;
|
|
126
|
+
}
|
|
95
127
|
async prepare_archive(project_path) {
|
|
96
128
|
try {
|
|
97
129
|
if (!fs.existsSync(project_path)) {
|
|
@@ -117,7 +149,7 @@ export default class Deploy extends Command {
|
|
|
117
149
|
"*.log",
|
|
118
150
|
];
|
|
119
151
|
const ignoreCache = {};
|
|
120
|
-
const ignoreInstance = ignore
|
|
152
|
+
const ignoreInstance = ignore({ ignorecase: false });
|
|
121
153
|
ignoreInstance.add(defaultIgnores);
|
|
122
154
|
const ignoreFN = (f) => {
|
|
123
155
|
const dir = dirname(f);
|
|
@@ -128,7 +160,7 @@ export default class Deploy extends Command {
|
|
|
128
160
|
if (!ignoreInstance.ignores(f)) {
|
|
129
161
|
return true;
|
|
130
162
|
}
|
|
131
|
-
if (
|
|
163
|
+
if (isDebug()) {
|
|
132
164
|
console.log(`ignoring ${f}`);
|
|
133
165
|
}
|
|
134
166
|
return false;
|
|
@@ -138,7 +170,7 @@ export default class Deploy extends Command {
|
|
|
138
170
|
fs.ensureDirSync(tmpDir);
|
|
139
171
|
const fileList = fs.readdirSync(project_path).filter(ignoreFN);
|
|
140
172
|
if (fileList.length === 0) {
|
|
141
|
-
throw new Error(`
|
|
173
|
+
throw new Error(`Nothing to upload from ${project_path} — the directory is empty, or every file in it is ignored. Check your .chabokignore, .dockerignore or .gitignore.`);
|
|
142
174
|
}
|
|
143
175
|
tar.c({
|
|
144
176
|
gzip: {
|
|
@@ -153,6 +185,7 @@ export default class Deploy extends Command {
|
|
|
153
185
|
}
|
|
154
186
|
catch (error) {
|
|
155
187
|
if (error instanceof Error) {
|
|
188
|
+
// eslint-disable-next-line unicorn/prefer-type-error -- this is an I/O failure, not a type check
|
|
156
189
|
throw new Error(`Failed to prepare archive: ${error.message}`);
|
|
157
190
|
}
|
|
158
191
|
throw new Error('Failed to prepare archive: Unknown error');
|
|
@@ -169,14 +202,17 @@ export default class Deploy extends Command {
|
|
|
169
202
|
const stats = fs.statSync(archive_path);
|
|
170
203
|
const sourceSize = stats.size;
|
|
171
204
|
if (sourceSize > MAX_SOURCE_SIZE) {
|
|
172
|
-
upload_response.message =
|
|
205
|
+
upload_response.message =
|
|
206
|
+
`Your project is ${formatBytes(sourceSize)} compressed, over the ${formatBytes(MAX_SOURCE_SIZE)} limit. ` +
|
|
207
|
+
`Add large files to .chabokignore and try again.`;
|
|
173
208
|
return upload_response;
|
|
174
209
|
}
|
|
175
|
-
|
|
210
|
+
cli.log(ui.info(`Uploading ${ui.value(formatBytes(sourceSize))} to ${ui.value(selected_service)}`));
|
|
211
|
+
const bar = new ProgressBar(" [:bar] :percent :etas left", {
|
|
176
212
|
total: sourceSize,
|
|
177
|
-
width:
|
|
178
|
-
complete: "
|
|
179
|
-
incomplete: "",
|
|
213
|
+
width: 24,
|
|
214
|
+
complete: "█",
|
|
215
|
+
incomplete: "░",
|
|
180
216
|
clear: true,
|
|
181
217
|
});
|
|
182
218
|
try {
|
|
@@ -188,11 +224,11 @@ export default class Deploy extends Command {
|
|
|
188
224
|
.json();
|
|
189
225
|
if (response.success) {
|
|
190
226
|
upload_response.success = true;
|
|
191
|
-
upload_response.message = `
|
|
227
|
+
upload_response.message = `Deployed to ${ui.value(selected_service)}.`;
|
|
192
228
|
}
|
|
193
229
|
else {
|
|
194
|
-
upload_response.message = `Deployment failed
|
|
195
|
-
if (
|
|
230
|
+
upload_response.message = `Deployment failed. ${String(response.message || "The server did not say why — try again.")}`;
|
|
231
|
+
if (isDebug()) {
|
|
196
232
|
cli.log(JSON.stringify(response, null, 2));
|
|
197
233
|
}
|
|
198
234
|
}
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import Command from "../base.js";
|
|
2
2
|
export default class Login extends Command {
|
|
3
3
|
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
description: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}[];
|
|
4
8
|
static flags: {
|
|
5
|
-
username: import("@oclif/core/
|
|
6
|
-
password: import("@oclif/core/
|
|
7
|
-
token: import("@oclif/core/
|
|
8
|
-
help: import("@oclif/core/
|
|
9
|
+
username: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
password: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
help: import("@oclif/core/interfaces").BooleanFlag<void>;
|
|
9
13
|
};
|
|
10
14
|
run(): Promise<void>;
|
|
11
15
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import axios from "axios";
|
|
4
|
-
import
|
|
4
|
+
import * as ui from "../ui.js";
|
|
5
5
|
import { isObject, handleApiError, logErrorDetails } from "../helper.js";
|
|
6
6
|
import Command from "../base.js";
|
|
7
7
|
export default class Login extends Command {
|
|
8
|
-
static description = '
|
|
8
|
+
static description = 'sign in to your hub.chabokan.net account';
|
|
9
|
+
static examples = [
|
|
10
|
+
{
|
|
11
|
+
description: 'Sign in interactively (prompts for email and password)',
|
|
12
|
+
command: '<%= config.bin %> login',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
description: 'Sign in with an API token — the right choice for CI',
|
|
16
|
+
command: '<%= config.bin %> login --token <your-api-token>',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
description: 'Pass the email up front and be prompted only for the password',
|
|
20
|
+
command: '<%= config.bin %> login --email me@example.com',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
description: 'Add a second account (both stay signed in; switch with `account use`)',
|
|
24
|
+
command: '<%= config.bin %> login --email other@example.com',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
9
27
|
static flags = {
|
|
10
28
|
...Command.flags,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
// The API still calls this field "username", but it only accepts emails;
|
|
30
|
+
// --email is accepted as an alias so the flag matches what users type.
|
|
31
|
+
username: Flags.string({ char: 'u', aliases: ['email'], description: 'your email address' }),
|
|
32
|
+
password: Flags.string({ char: 'p', description: 'your password (prompted for if omitted)' }),
|
|
33
|
+
token: Flags.string({ char: 't', description: 'API token, for non-interactive sign-in' }),
|
|
14
34
|
};
|
|
15
35
|
async run() {
|
|
16
36
|
const { flags } = await this.parse(Login);
|
|
@@ -21,15 +41,49 @@ export default class Login extends Command {
|
|
|
21
41
|
username: flags.username,
|
|
22
42
|
password: flags.password
|
|
23
43
|
};
|
|
24
|
-
if (
|
|
44
|
+
if (flags.token) {
|
|
45
|
+
try {
|
|
46
|
+
if (!this.axiosConfig.headers) {
|
|
47
|
+
this.axiosConfig.headers = {};
|
|
48
|
+
}
|
|
49
|
+
this.axiosConfig.headers.Authorization = `Token ${flags.token}`;
|
|
50
|
+
const { data } = await axios.get("accounts/info/", this.axiosConfig);
|
|
51
|
+
if (!isObject(config_json.users)) {
|
|
52
|
+
config_json.users = {};
|
|
53
|
+
}
|
|
54
|
+
const { email } = data.user;
|
|
55
|
+
config_json.users[email] = {
|
|
56
|
+
token: flags.token
|
|
57
|
+
};
|
|
58
|
+
config_json.default_user = email;
|
|
59
|
+
await this.write_config(config_json);
|
|
60
|
+
cli.log(ui.success(`Signed in as ${ui.value(email)} using an API token.`));
|
|
61
|
+
cli.log(ui.hint(`See what you can reach with ${ui.cmd('chabok service list')}`));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const errorInfo = handleApiError(error, 'That API token was not accepted.', {
|
|
65
|
+
endpoint: 'accounts/info/',
|
|
66
|
+
operation: 'Token Login'
|
|
67
|
+
});
|
|
68
|
+
cli.log(ui.error(errorInfo.message));
|
|
69
|
+
cli.log(ui.hint('Generate a token from your account settings at https://hub.chabokan.net'));
|
|
70
|
+
logErrorDetails(errorInfo, cli);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
25
74
|
if (!flags.username) {
|
|
26
75
|
const { username } = await inquirer.prompt({
|
|
27
76
|
type: 'input',
|
|
28
|
-
message: '
|
|
77
|
+
message: 'Email address:',
|
|
29
78
|
name: 'username',
|
|
30
79
|
validate(input) {
|
|
31
80
|
if (input.length === 0) {
|
|
32
|
-
return '
|
|
81
|
+
return 'Email address cannot be empty';
|
|
82
|
+
}
|
|
83
|
+
// Basic email validation
|
|
84
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
85
|
+
if (!emailRegex.test(input)) {
|
|
86
|
+
return 'Please enter a valid email address';
|
|
33
87
|
}
|
|
34
88
|
return true;
|
|
35
89
|
}
|
|
@@ -40,7 +94,7 @@ export default class Login extends Command {
|
|
|
40
94
|
const { password } = await inquirer.prompt({
|
|
41
95
|
type: 'password',
|
|
42
96
|
name: 'password',
|
|
43
|
-
message: '
|
|
97
|
+
message: 'Password:',
|
|
44
98
|
validate(input) {
|
|
45
99
|
if (input.length === 0) {
|
|
46
100
|
return 'Password cannot be empty';
|
|
@@ -51,7 +105,7 @@ export default class Login extends Command {
|
|
|
51
105
|
body.password = password;
|
|
52
106
|
}
|
|
53
107
|
if (!body.username || !body.password) {
|
|
54
|
-
cli.log(
|
|
108
|
+
cli.log(ui.error('Both an email address and a password are required.'));
|
|
55
109
|
return;
|
|
56
110
|
}
|
|
57
111
|
try {
|
|
@@ -65,45 +119,19 @@ export default class Login extends Command {
|
|
|
65
119
|
};
|
|
66
120
|
config_json.default_user = body.username;
|
|
67
121
|
await this.write_config(config_json);
|
|
68
|
-
cli.log(
|
|
122
|
+
cli.log(ui.success(`Signed in as ${ui.value(body.username)}.`));
|
|
123
|
+
cli.log(ui.hint(`See what you can reach with ${ui.cmd('chabok service list')}`));
|
|
69
124
|
}
|
|
70
125
|
else {
|
|
71
|
-
cli.log(
|
|
126
|
+
cli.log(ui.error('That email address and password did not match an account.'));
|
|
72
127
|
}
|
|
73
128
|
}
|
|
74
129
|
catch (error) {
|
|
75
|
-
const errorInfo = handleApiError(error, '
|
|
130
|
+
const errorInfo = handleApiError(error, 'Sign-in failed. Check your credentials and try again.', {
|
|
76
131
|
endpoint: 'accounts/login/',
|
|
77
132
|
operation: 'Login'
|
|
78
133
|
});
|
|
79
|
-
cli.log(
|
|
80
|
-
logErrorDetails(errorInfo, cli);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
try {
|
|
85
|
-
if (!this.axiosConfig.headers) {
|
|
86
|
-
this.axiosConfig.headers = {};
|
|
87
|
-
}
|
|
88
|
-
this.axiosConfig.headers.Authorization = `Token ${flags.token}`;
|
|
89
|
-
const { data } = await axios.get("accounts/info/", this.axiosConfig);
|
|
90
|
-
if (!isObject(config_json.users)) {
|
|
91
|
-
config_json.users = {};
|
|
92
|
-
}
|
|
93
|
-
const email = data.user.email;
|
|
94
|
-
config_json.users[email] = {
|
|
95
|
-
token: flags.token
|
|
96
|
-
};
|
|
97
|
-
config_json.default_user = email;
|
|
98
|
-
await this.write_config(config_json);
|
|
99
|
-
cli.log(`${chalk.green('[Success]')} You are logged in successfully as '${email}' using API token.`);
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
const errorInfo = handleApiError(error, 'Invalid API token. Please verify your token and try again.', {
|
|
103
|
-
endpoint: 'accounts/info/',
|
|
104
|
-
operation: 'Token Login'
|
|
105
|
-
});
|
|
106
|
-
cli.log(`${chalk.red('[Error]')} ${errorInfo.message}`);
|
|
134
|
+
cli.log(ui.error(errorInfo.message));
|
|
107
135
|
logErrorDetails(errorInfo, cli);
|
|
108
136
|
}
|
|
109
137
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Command from "../../../base.js";
|
|
2
|
+
export default class ServiceDomainAdd extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
description: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}[];
|
|
8
|
+
static flags: {
|
|
9
|
+
service: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
help: import("@oclif/core/interfaces").BooleanFlag<void>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
send_request(cli: Command, selected_service: string, domain_name: string): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import Command from "../../../base.js";
|
|
3
|
+
import { handleApiError, logErrorDetails } from "../../../helper.js";
|
|
4
|
+
import * as ui from "../../../ui.js";
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
export default class ServiceDomainAdd extends Command {
|
|
8
|
+
static description = 'connect a custom domain to a service';
|
|
9
|
+
static examples = [
|
|
10
|
+
{
|
|
11
|
+
description: 'Pick the service and enter the domain interactively',
|
|
12
|
+
command: '<%= config.bin %> service domain add',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
description: 'Connect a domain in one non-interactive command',
|
|
16
|
+
command: '<%= config.bin %> service domain add -s my-app -d www.example.com',
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
static flags = {
|
|
20
|
+
...Command.flags,
|
|
21
|
+
service: Flags.string({ char: 's', description: 'name of the service to connect the domain to' }),
|
|
22
|
+
domain: Flags.string({ char: 'd', description: 'domain name to connect, e.g. www.example.com' }),
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { flags } = await this.parse(ServiceDomainAdd);
|
|
26
|
+
const cli = this;
|
|
27
|
+
await this.init_run();
|
|
28
|
+
if (!(await this.require_login())) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const selected_service = await this.select_service(flags.service, {}, 'Connect Domain');
|
|
32
|
+
if (!selected_service) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let domain_name = flags.domain;
|
|
36
|
+
if (!domain_name) {
|
|
37
|
+
({ domain_name } = await inquirer.prompt({
|
|
38
|
+
type: 'input',
|
|
39
|
+
message: 'Domain name:',
|
|
40
|
+
name: 'domain_name',
|
|
41
|
+
validate(input) {
|
|
42
|
+
return input.trim().length > 0 || 'Domain name cannot be empty';
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
await this.send_request(cli, selected_service, domain_name.trim());
|
|
47
|
+
}
|
|
48
|
+
async send_request(cli, selected_service, domain_name) {
|
|
49
|
+
try {
|
|
50
|
+
const { data } = await axios.post(`services/${selected_service}/domain/`, { domain_name }, this.axiosConfig);
|
|
51
|
+
if (data.success === false) {
|
|
52
|
+
cli.log(ui.error(`Could not connect ${ui.value(domain_name)}. ${data.message || 'Check the domain name and try again.'}`));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
cli.log(ui.success(`Connected ${ui.value(domain_name)} to ${ui.value(selected_service)}.`));
|
|
56
|
+
cli.log(ui.hint('Point the domain\'s DNS records at Chabokan for it to start serving traffic'));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const errorInfo = handleApiError(error, 'Failed to connect domain.', {
|
|
60
|
+
endpoint: `services/${selected_service}/domain/`,
|
|
61
|
+
serviceName: selected_service,
|
|
62
|
+
operation: 'Connect Domain'
|
|
63
|
+
});
|
|
64
|
+
if (errorInfo.status === 404) {
|
|
65
|
+
cli.log(ui.error(`Service ${ui.value(selected_service)} was not found.`));
|
|
66
|
+
cli.log(ui.hint(`Check the available names with ${ui.cmd('chabok service list')}`));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
cli.log(ui.error(errorInfo.message));
|
|
70
|
+
}
|
|
71
|
+
logErrorDetails(errorInfo, cli);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Command from "../../../base.js";
|
|
2
|
+
export default class ServiceDomainRemove extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
description: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}[];
|
|
8
|
+
static flags: {
|
|
9
|
+
service: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
help: import("@oclif/core/interfaces").BooleanFlag<void>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
send_request(cli: Command, selected_service: string, domain_name: string): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import Command from "../../../base.js";
|
|
3
|
+
import { handleApiError, logErrorDetails } from "../../../helper.js";
|
|
4
|
+
import * as ui from "../../../ui.js";
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
export default class ServiceDomainRemove extends Command {
|
|
8
|
+
static description = 'disconnect a custom domain from a service';
|
|
9
|
+
static examples = [
|
|
10
|
+
{
|
|
11
|
+
description: 'Pick the service and enter the domain interactively',
|
|
12
|
+
command: '<%= config.bin %> service domain remove',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
description: 'Disconnect a domain in one non-interactive command',
|
|
16
|
+
command: '<%= config.bin %> service domain remove -s my-app -d www.example.com',
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
static flags = {
|
|
20
|
+
...Command.flags,
|
|
21
|
+
service: Flags.string({ char: 's', description: 'name of the service the domain is connected to' }),
|
|
22
|
+
domain: Flags.string({ char: 'd', description: 'domain name to disconnect' }),
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { flags } = await this.parse(ServiceDomainRemove);
|
|
26
|
+
const cli = this;
|
|
27
|
+
await this.init_run();
|
|
28
|
+
if (!(await this.require_login())) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const selected_service = await this.select_service(flags.service, {}, 'Disconnect Domain');
|
|
32
|
+
if (!selected_service) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let domain_name = flags.domain;
|
|
36
|
+
if (!domain_name) {
|
|
37
|
+
({ domain_name } = await inquirer.prompt({
|
|
38
|
+
type: 'input',
|
|
39
|
+
message: 'Domain name:',
|
|
40
|
+
name: 'domain_name',
|
|
41
|
+
validate(input) {
|
|
42
|
+
return input.trim().length > 0 || 'Domain name cannot be empty';
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
await this.send_request(cli, selected_service, domain_name.trim());
|
|
47
|
+
}
|
|
48
|
+
async send_request(cli, selected_service, domain_name) {
|
|
49
|
+
try {
|
|
50
|
+
const { data } = await axios.delete(`services/${selected_service}/domain/${domain_name}/`, this.axiosConfig);
|
|
51
|
+
if (data?.success === false) {
|
|
52
|
+
cli.log(ui.error(`Could not disconnect ${ui.value(domain_name)}. ${data.message || 'Check the domain name and try again.'}`));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
cli.log(ui.success(`Disconnected ${ui.value(domain_name)} from ${ui.value(selected_service)}.`));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const errorInfo = handleApiError(error, 'Failed to disconnect domain.', {
|
|
59
|
+
endpoint: `services/${selected_service}/domain/${domain_name}/`,
|
|
60
|
+
serviceName: selected_service,
|
|
61
|
+
operation: 'Disconnect Domain'
|
|
62
|
+
});
|
|
63
|
+
if (errorInfo.status === 404) {
|
|
64
|
+
cli.log(ui.error(`Domain ${ui.value(domain_name)} was not found on ${ui.value(selected_service)}.`));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
cli.log(ui.error(errorInfo.message));
|
|
68
|
+
}
|
|
69
|
+
logErrorDetails(errorInfo, cli);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import Command from "../../base.js";
|
|
2
2
|
export default class ServiceList extends Command {
|
|
3
3
|
static description: string;
|
|
4
|
+
static examples: {
|
|
5
|
+
description: string;
|
|
6
|
+
command: string;
|
|
7
|
+
}[];
|
|
4
8
|
static flags: {
|
|
5
|
-
help: import("@oclif/core/
|
|
9
|
+
help: import("@oclif/core/interfaces").BooleanFlag<void>;
|
|
6
10
|
};
|
|
7
11
|
run(): Promise<void>;
|
|
8
12
|
}
|