@chabokan.net/cli 0.8.10 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +520 -66
  2. package/dist/base.d.ts +34 -9
  3. package/dist/base.js +172 -20
  4. package/dist/commands/account/info.d.ts +12 -0
  5. package/dist/commands/account/info.js +40 -0
  6. package/dist/commands/account/list.d.ts +5 -1
  7. package/dist/commands/account/list.js +35 -16
  8. package/dist/commands/account/remove.d.ts +5 -1
  9. package/dist/commands/account/remove.js +30 -15
  10. package/dist/commands/account/use.d.ts +6 -2
  11. package/dist/commands/account/use.js +26 -15
  12. package/dist/commands/cloudserver/create.d.ts +40 -0
  13. package/dist/commands/cloudserver/create.js +242 -0
  14. package/dist/commands/cloudserver/delete.d.ts +14 -0
  15. package/dist/commands/cloudserver/delete.js +65 -0
  16. package/dist/commands/cloudserver/list.d.ts +12 -0
  17. package/dist/commands/cloudserver/list.js +46 -0
  18. package/dist/commands/cloudserver/restart.d.ts +13 -0
  19. package/dist/commands/cloudserver/restart.js +51 -0
  20. package/dist/commands/cloudserver/start.d.ts +13 -0
  21. package/dist/commands/cloudserver/start.js +51 -0
  22. package/dist/commands/cloudserver/stop.d.ts +13 -0
  23. package/dist/commands/cloudserver/stop.js +52 -0
  24. package/dist/commands/deploy.d.ts +16 -5
  25. package/dist/commands/deploy.js +182 -101
  26. package/dist/commands/login.d.ts +8 -4
  27. package/dist/commands/login.js +97 -49
  28. package/dist/commands/service/domain/add.d.ts +15 -0
  29. package/dist/commands/service/domain/add.js +74 -0
  30. package/dist/commands/service/domain/remove.d.ts +15 -0
  31. package/dist/commands/service/domain/remove.js +72 -0
  32. package/dist/commands/service/list.d.ts +5 -1
  33. package/dist/commands/service/list.js +41 -20
  34. package/dist/commands/service/logs.d.ts +7 -3
  35. package/dist/commands/service/logs.js +46 -34
  36. package/dist/commands/service/resize.d.ts +10 -6
  37. package/dist/commands/service/resize.js +94 -72
  38. package/dist/commands/service/restart.d.ts +7 -3
  39. package/dist/commands/service/restart.js +40 -31
  40. package/dist/commands/service/start.d.ts +7 -3
  41. package/dist/commands/service/start.js +41 -30
  42. package/dist/commands/service/stop.d.ts +7 -3
  43. package/dist/commands/service/stop.js +41 -30
  44. package/dist/commands/wallet/list.d.ts +12 -0
  45. package/dist/commands/wallet/list.js +41 -0
  46. package/dist/constants.d.ts +2 -0
  47. package/dist/constants.js +7 -2
  48. package/dist/helper.d.ts +32 -9
  49. package/dist/helper.js +426 -49
  50. package/dist/types.d.ts +77 -0
  51. package/dist/types.js +2 -0
  52. package/dist/ui.d.ts +23 -0
  53. package/dist/ui.js +70 -0
  54. package/oclif.manifest.json +635 -25
  55. package/package.json +39 -35
@@ -1,144 +1,218 @@
1
1
  import { Flags } from "@oclif/core";
2
- import inquirer from "inquirer";
3
- import chalk from "chalk";
2
+ import * as ui from "../ui.js";
4
3
  import Command from "../base.js";
5
- import { addIgnorePatterns, isEmptyObject } from "../helper.js";
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 = "this command help you build and deploy your service to chabokan in easy way.";
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: "service path in your computer",
55
+ description: "project directory to upload (defaults to the current directory)",
22
56
  }),
23
- service: Flags.string({ char: "s", description: "service name" }),
57
+ service: Flags.string({ char: "s", description: "name of the service to deploy to" }),
24
58
  };
25
59
  async run() {
26
- const { args, flags } = await this.parse(Deploy);
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
- let project_path = flags.path ? flags.path : process.cwd();
32
- let chabok_file = { service: "" };
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 (isEmptyObject(config_json.users)) {
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
- let all_services = await this.get_services({
50
- has_deploy: "true",
51
- status: "on",
52
- });
53
- if (all_services.length > 0) {
54
- let { service } = await inquirer.prompt({
55
- type: "list",
56
- message: "Please select a service:",
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("first you should create service");
64
- return;
84
+ cli.log(ui.error(upload_response.message));
65
85
  }
66
86
  }
67
- let archive_path = await this.prepare_archive(project_path);
68
- let upload_response = await this.upload(archive_path, selected_service, cli, chabok_file);
69
- if (upload_response.success) {
70
- cli.log(chalk.green(upload_response.message));
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
- else {
73
- cli.log(chalk.red("[Error] " + upload_response.message));
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
- const defaultIgnores = [
78
- ".npm",
79
- ".git",
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 (!ignoreInstance.ignores(f)) {
103
- return true;
132
+ if (!fs.statSync(project_path).isDirectory()) {
133
+ throw new Error(`Project path is not a directory: ${project_path}`);
104
134
  }
105
- console.log(`ignoring ${f}`);
106
- return false;
107
- };
108
- const tmpDir = path.join(os.tmpdir(), "/chabok-cli");
109
- const archivePath = path.join(tmpDir, `${Date.now()}.tar.gz`);
110
- fs.ensureDirSync(tmpDir);
111
- const fileList = fs.readdirSync(project_path).filter(ignoreFN);
112
- tar.c({
113
- gzip: {
114
- level: 9,
115
- },
116
- sync: true,
117
- cwd: project_path,
118
- filter: ignoreFN,
119
- file: archivePath,
120
- }, fileList);
121
- return archivePath;
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
- let upload_response = {
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 { size: sourceSize } = fs.statSync(archive_path);
202
+ const stats = fs.statSync(archive_path);
203
+ const sourceSize = stats.size;
133
204
  if (sourceSize > MAX_SOURCE_SIZE) {
134
- upload_response.message = "Source is too large. (max: 100MB)";
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
- const bar = new ProgressBar("Uploading [:bar] :percent :etas", {
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: 20,
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 = "Deployment finished successfully.";
227
+ upload_response.message = `Deployed to ${ui.value(selected_service)}.`;
154
228
  }
155
229
  else {
156
- if (process.env.CHABOK_DEBUG == "true") {
157
- cli.log(response);
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
- if (process.env.CHABOK_DEBUG == "true") {
164
- cli.log(error);
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
  }
@@ -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/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
6
- password: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
7
- token: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
- help: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<void>;
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
  }
@@ -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 chalk from "chalk";
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 = 'login to hub.chabokan.net account';
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
- username: Flags.string({ char: 'u', description: 'your username' }),
12
- password: Flags.string({ char: 'p', description: 'your password' }),
13
- token: Flags.string({ char: 't', description: 'login with api token' }),
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 { args, flags } = await this.parse(Login);
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 = { username: flags.username, password: flags.password };
21
- if (!flags.token) {
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
- let { username } = await inquirer.prompt({
75
+ const { username } = await inquirer.prompt({
24
76
  type: 'input',
25
- message: 'Enter your username:',
77
+ message: 'Email address:',
26
78
  name: 'username',
27
79
  validate(input) {
28
80
  if (input.length === 0) {
29
- return false;
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
- let { password } = await inquirer.prompt({
94
+ const { password } = await inquirer.prompt({
38
95
  type: 'password',
39
96
  name: 'password',
40
- message: 'Enter your password:',
97
+ message: 'Password:',
41
98
  validate(input) {
42
99
  if (input.length === 0) {
43
- return false;
100
+ return 'Password cannot be empty';
44
101
  }
45
102
  return true;
46
103
  }
47
104
  });
48
105
  body.password = password;
49
106
  }
50
- const { data } = await axios.post("accounts/login/", body, this.axiosConfig);
51
- if (data.success) {
52
- if (!isObject(config_json.users)) {
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
- this.axiosConfig.headers.Authorization = `Token ${flags.token}`;
70
- const { data } = await axios.get("accounts/info/", this.axiosConfig);
71
- if (!isObject(config_json.users)) {
72
- config_json.users = {};
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
- // @ts-ignore
75
- config_json.users[data.user.email] = {
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
- cli.log(`${chalk.red('[Error]')} your token is wrong!`);
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
+ }