@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/commands/deploy.js
CHANGED
|
@@ -1,144 +1,218 @@
|
|
|
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
|
-
const {
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
chabok_file =
|
|
35
|
-
JSON.parse(fs
|
|
36
|
-
.readFileSync(path.join(project_path, "chabok.json"))
|
|
37
|
-
.toString("utf-8")) || {};
|
|
38
|
-
cli.log("Reading Config from chabok.json ...");
|
|
39
|
-
}
|
|
40
|
-
catch { }
|
|
64
|
+
const project_path = flags.path ? flags.path : process.cwd();
|
|
65
|
+
const chabok_file = this.read_chabok_file(project_path);
|
|
41
66
|
if (chabok_file.service) {
|
|
42
67
|
selected_service = chabok_file.service;
|
|
43
68
|
}
|
|
44
|
-
if (
|
|
45
|
-
cli.log(`${chalk.red("[Error]")} first you should login!`);
|
|
69
|
+
if (!(await this.require_login())) {
|
|
46
70
|
return;
|
|
47
71
|
}
|
|
72
|
+
selected_service = await this.select_service(selected_service, { has_deploy: "true", status: "on" }, "Deploy");
|
|
48
73
|
if (!selected_service) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
name: "service",
|
|
58
|
-
choices: all_services,
|
|
59
|
-
});
|
|
60
|
-
selected_service = service;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const archive_path = await this.prepare_archive(project_path);
|
|
78
|
+
const upload_response = await this.upload(archive_path, selected_service, cli, chabok_file);
|
|
79
|
+
if (upload_response.success) {
|
|
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}`)}`));
|
|
61
82
|
}
|
|
62
83
|
else {
|
|
63
|
-
cli.log(
|
|
64
|
-
return;
|
|
84
|
+
cli.log(ui.error(upload_response.message));
|
|
65
85
|
}
|
|
66
86
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
catch (error) {
|
|
88
|
+
const errorInfo = handleApiError(error, 'Deployment failed. Check your project files and try again.', {
|
|
89
|
+
endpoint: `services/${selected_service}/deploy/`,
|
|
90
|
+
serviceName: selected_service,
|
|
91
|
+
operation: 'Deploy'
|
|
92
|
+
});
|
|
93
|
+
cli.log(ui.error(errorInfo.message));
|
|
94
|
+
logErrorDetails(errorInfo, cli);
|
|
95
|
+
}
|
|
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: "" };
|
|
71
118
|
}
|
|
72
|
-
|
|
73
|
-
|
|
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;
|
|
74
123
|
}
|
|
124
|
+
this.log(ui.info('Using settings from chabok.json'));
|
|
125
|
+
return chabok_file;
|
|
75
126
|
}
|
|
76
127
|
async prepare_archive(project_path) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
".idea",
|
|
81
|
-
".DS_Store",
|
|
82
|
-
".vscode",
|
|
83
|
-
"__pycache__",
|
|
84
|
-
".next",
|
|
85
|
-
".nuxt",
|
|
86
|
-
"*.*~",
|
|
87
|
-
"node_modules",
|
|
88
|
-
"bower_components",
|
|
89
|
-
"venv",
|
|
90
|
-
"/vendor",
|
|
91
|
-
"*.log",
|
|
92
|
-
];
|
|
93
|
-
const ignoreCache = {};
|
|
94
|
-
const ignoreInstance = ignore.default({ ignorecase: false });
|
|
95
|
-
ignoreInstance.add(defaultIgnores);
|
|
96
|
-
const ignoreFN = (f) => {
|
|
97
|
-
const dir = dirname(f);
|
|
98
|
-
if (!ignoreCache[dir]) {
|
|
99
|
-
addIgnorePatterns(ignoreInstance, project_path, dir);
|
|
100
|
-
ignoreCache[dir] = true;
|
|
128
|
+
try {
|
|
129
|
+
if (!fs.existsSync(project_path)) {
|
|
130
|
+
throw new Error(`Project path does not exist: ${project_path}`);
|
|
101
131
|
}
|
|
102
|
-
if (!
|
|
103
|
-
|
|
132
|
+
if (!fs.statSync(project_path).isDirectory()) {
|
|
133
|
+
throw new Error(`Project path is not a directory: ${project_path}`);
|
|
104
134
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
const defaultIgnores = [
|
|
136
|
+
".npm",
|
|
137
|
+
".git",
|
|
138
|
+
".idea",
|
|
139
|
+
".DS_Store",
|
|
140
|
+
".vscode",
|
|
141
|
+
"__pycache__",
|
|
142
|
+
".next",
|
|
143
|
+
".nuxt",
|
|
144
|
+
"*.*~",
|
|
145
|
+
"node_modules",
|
|
146
|
+
"bower_components",
|
|
147
|
+
"venv",
|
|
148
|
+
"/vendor",
|
|
149
|
+
"*.log",
|
|
150
|
+
];
|
|
151
|
+
const ignoreCache = {};
|
|
152
|
+
const ignoreInstance = ignore({ ignorecase: false });
|
|
153
|
+
ignoreInstance.add(defaultIgnores);
|
|
154
|
+
const ignoreFN = (f) => {
|
|
155
|
+
const dir = dirname(f);
|
|
156
|
+
if (!ignoreCache[dir]) {
|
|
157
|
+
addIgnorePatterns(ignoreInstance, project_path, dir);
|
|
158
|
+
ignoreCache[dir] = true;
|
|
159
|
+
}
|
|
160
|
+
if (!ignoreInstance.ignores(f)) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (isDebug()) {
|
|
164
|
+
console.log(`ignoring ${f}`);
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
};
|
|
168
|
+
const tmpDir = path.join(os.tmpdir(), "/chabok-cli");
|
|
169
|
+
const archivePath = path.join(tmpDir, `${Date.now()}.tar.gz`);
|
|
170
|
+
fs.ensureDirSync(tmpDir);
|
|
171
|
+
const fileList = fs.readdirSync(project_path).filter(ignoreFN);
|
|
172
|
+
if (fileList.length === 0) {
|
|
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.`);
|
|
174
|
+
}
|
|
175
|
+
tar.c({
|
|
176
|
+
gzip: {
|
|
177
|
+
level: 9,
|
|
178
|
+
},
|
|
179
|
+
sync: true,
|
|
180
|
+
cwd: project_path,
|
|
181
|
+
filter: ignoreFN,
|
|
182
|
+
file: archivePath,
|
|
183
|
+
}, fileList);
|
|
184
|
+
return archivePath;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (error instanceof Error) {
|
|
188
|
+
// eslint-disable-next-line unicorn/prefer-type-error -- this is an I/O failure, not a type check
|
|
189
|
+
throw new Error(`Failed to prepare archive: ${error.message}`);
|
|
190
|
+
}
|
|
191
|
+
throw new Error('Failed to prepare archive: Unknown error');
|
|
192
|
+
}
|
|
122
193
|
}
|
|
123
194
|
async upload(archive_path, selected_service, cli, chabok_file) {
|
|
124
|
-
|
|
195
|
+
const upload_response = {
|
|
125
196
|
success: false,
|
|
126
197
|
message: "some problem",
|
|
127
198
|
};
|
|
128
199
|
const body = new FormData();
|
|
129
|
-
// @ts-ignore
|
|
130
200
|
body.append("file", fs.createReadStream(archive_path));
|
|
131
201
|
body.append("options", JSON.stringify(chabok_file));
|
|
132
|
-
const
|
|
202
|
+
const stats = fs.statSync(archive_path);
|
|
203
|
+
const sourceSize = stats.size;
|
|
133
204
|
if (sourceSize > MAX_SOURCE_SIZE) {
|
|
134
|
-
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.`;
|
|
135
208
|
return upload_response;
|
|
136
209
|
}
|
|
137
|
-
|
|
210
|
+
cli.log(ui.info(`Uploading ${ui.value(formatBytes(sourceSize))} to ${ui.value(selected_service)}`));
|
|
211
|
+
const bar = new ProgressBar(" [:bar] :percent :etas left", {
|
|
138
212
|
total: sourceSize,
|
|
139
|
-
width:
|
|
140
|
-
complete: "
|
|
141
|
-
incomplete: "",
|
|
213
|
+
width: 24,
|
|
214
|
+
complete: "█",
|
|
215
|
+
incomplete: "░",
|
|
142
216
|
clear: true,
|
|
143
217
|
});
|
|
144
218
|
try {
|
|
@@ -150,24 +224,31 @@ export default class Deploy extends Command {
|
|
|
150
224
|
.json();
|
|
151
225
|
if (response.success) {
|
|
152
226
|
upload_response.success = true;
|
|
153
|
-
upload_response.message =
|
|
227
|
+
upload_response.message = `Deployed to ${ui.value(selected_service)}.`;
|
|
154
228
|
}
|
|
155
229
|
else {
|
|
156
|
-
|
|
157
|
-
|
|
230
|
+
upload_response.message = `Deployment failed. ${String(response.message || "The server did not say why — try again.")}`;
|
|
231
|
+
if (isDebug()) {
|
|
232
|
+
cli.log(JSON.stringify(response, null, 2));
|
|
158
233
|
}
|
|
159
234
|
}
|
|
160
235
|
return upload_response;
|
|
161
236
|
}
|
|
162
237
|
catch (error) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
238
|
+
const errorInfo = handleApiError(error, 'Deployment failed. Please check your project files and network connection.', {
|
|
239
|
+
endpoint: `services/${selected_service}/deploy/`,
|
|
240
|
+
serviceName: selected_service,
|
|
241
|
+
operation: 'Upload Deployment'
|
|
242
|
+
});
|
|
243
|
+
upload_response.message = errorInfo.message;
|
|
244
|
+
logErrorDetails(errorInfo, cli);
|
|
166
245
|
return upload_response;
|
|
167
246
|
}
|
|
168
247
|
finally {
|
|
169
248
|
// cleanup
|
|
170
|
-
fs.unlink(archive_path).catch(() => {
|
|
249
|
+
fs.unlink(archive_path).catch(() => {
|
|
250
|
+
// Ignore cleanup errors
|
|
251
|
+
});
|
|
171
252
|
}
|
|
172
253
|
}
|
|
173
254
|
}
|
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,32 +1,89 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import axios from "axios";
|
|
4
|
-
import
|
|
5
|
-
import { isObject } from "../helper.js";
|
|
4
|
+
import * as ui from "../ui.js";
|
|
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
|
-
const {
|
|
36
|
+
const { flags } = await this.parse(Login);
|
|
17
37
|
const cli = this;
|
|
18
38
|
await this.init_run();
|
|
19
39
|
const config_json = await this.read_config();
|
|
20
|
-
const body = {
|
|
21
|
-
|
|
40
|
+
const body = {
|
|
41
|
+
username: flags.username,
|
|
42
|
+
password: flags.password
|
|
43
|
+
};
|
|
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 {
|
|
22
74
|
if (!flags.username) {
|
|
23
|
-
|
|
75
|
+
const { username } = await inquirer.prompt({
|
|
24
76
|
type: 'input',
|
|
25
|
-
message: '
|
|
77
|
+
message: 'Email address:',
|
|
26
78
|
name: 'username',
|
|
27
79
|
validate(input) {
|
|
28
80
|
if (input.length === 0) {
|
|
29
|
-
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';
|
|
30
87
|
}
|
|
31
88
|
return true;
|
|
32
89
|
}
|
|
@@ -34,57 +91,48 @@ export default class Login extends Command {
|
|
|
34
91
|
body.username = username;
|
|
35
92
|
}
|
|
36
93
|
if (!flags.password) {
|
|
37
|
-
|
|
94
|
+
const { password } = await inquirer.prompt({
|
|
38
95
|
type: 'password',
|
|
39
96
|
name: 'password',
|
|
40
|
-
message: '
|
|
97
|
+
message: 'Password:',
|
|
41
98
|
validate(input) {
|
|
42
99
|
if (input.length === 0) {
|
|
43
|
-
return
|
|
100
|
+
return 'Password cannot be empty';
|
|
44
101
|
}
|
|
45
102
|
return true;
|
|
46
103
|
}
|
|
47
104
|
});
|
|
48
105
|
body.password = password;
|
|
49
106
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
config_json.users = {};
|
|
54
|
-
}
|
|
55
|
-
// @ts-ignore
|
|
56
|
-
config_json.users[body.username] = {
|
|
57
|
-
"token": data.token
|
|
58
|
-
};
|
|
59
|
-
config_json["default_user"] = body.username;
|
|
60
|
-
this.write_config(config_json);
|
|
61
|
-
cli.log(`${chalk.green('[Success]')} you are logged in successfully.`);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
cli.log(`${chalk.red('[Error]')} your username or password is wrong!`);
|
|
107
|
+
if (!body.username || !body.password) {
|
|
108
|
+
cli.log(ui.error('Both an email address and a password are required.'));
|
|
109
|
+
return;
|
|
65
110
|
}
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
111
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
112
|
+
const { data } = await axios.post("accounts/login/", body, this.axiosConfig);
|
|
113
|
+
if (data.success && data.token) {
|
|
114
|
+
if (!isObject(config_json.users)) {
|
|
115
|
+
config_json.users = {};
|
|
116
|
+
}
|
|
117
|
+
config_json.users[body.username] = {
|
|
118
|
+
token: data.token
|
|
119
|
+
};
|
|
120
|
+
config_json.default_user = body.username;
|
|
121
|
+
await this.write_config(config_json);
|
|
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')}`));
|
|
73
124
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"token": flags.token
|
|
77
|
-
};
|
|
78
|
-
config_json["default_user"] = data.user.email;
|
|
79
|
-
await this.write_config(config_json);
|
|
80
|
-
cli.log(`${chalk.green('[Success]')} you are logged in successfully.`);
|
|
81
|
-
}
|
|
82
|
-
catch (e) {
|
|
83
|
-
if (process.env.CHABOK_DEBUG == "true") {
|
|
84
|
-
// @ts-ignore
|
|
85
|
-
cli.log(e);
|
|
125
|
+
else {
|
|
126
|
+
cli.log(ui.error('That email address and password did not match an account.'));
|
|
86
127
|
}
|
|
87
|
-
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const errorInfo = handleApiError(error, 'Sign-in failed. Check your credentials and try again.', {
|
|
131
|
+
endpoint: 'accounts/login/',
|
|
132
|
+
operation: 'Login'
|
|
133
|
+
});
|
|
134
|
+
cli.log(ui.error(errorInfo.message));
|
|
135
|
+
logErrorDetails(errorInfo, cli);
|
|
88
136
|
}
|
|
89
137
|
}
|
|
90
138
|
}
|
|
@@ -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
|
+
}
|