@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.
Files changed (54) hide show
  1. package/README.md +520 -66
  2. package/dist/base.d.ts +30 -7
  3. package/dist/base.js +163 -14
  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 +29 -19
  8. package/dist/commands/account/remove.d.ts +5 -1
  9. package/dist/commands/account/remove.js +23 -12
  10. package/dist/commands/account/use.d.ts +6 -2
  11. package/dist/commands/account/use.js +22 -11
  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 +13 -3
  25. package/dist/commands/deploy.js +96 -60
  26. package/dist/commands/login.d.ts +8 -4
  27. package/dist/commands/login.js +69 -41
  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 +29 -21
  34. package/dist/commands/service/logs.d.ts +6 -2
  35. package/dist/commands/service/logs.js +33 -30
  36. package/dist/commands/service/resize.d.ts +9 -5
  37. package/dist/commands/service/resize.js +73 -69
  38. package/dist/commands/service/restart.d.ts +6 -2
  39. package/dist/commands/service/restart.js +28 -32
  40. package/dist/commands/service/start.d.ts +6 -2
  41. package/dist/commands/service/start.js +29 -31
  42. package/dist/commands/service/stop.d.ts +6 -2
  43. package/dist/commands/service/stop.js +29 -31
  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 +23 -7
  49. package/dist/helper.js +294 -113
  50. package/dist/types.d.ts +28 -8
  51. package/dist/ui.d.ts +23 -0
  52. package/dist/ui.js +70 -0
  53. package/oclif.manifest.json +635 -25
  54. package/package.json +39 -35
@@ -1,97 +1,129 @@
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, handleApiError, logErrorDetails } 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
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
- let chabok_file = { service: "" };
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 (isEmptyObject(config_json.users)) {
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(chalk.green(upload_response.message));
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(chalk.red(`[Error] ${upload_response.message}`));
84
+ cli.log(ui.error(upload_response.message));
83
85
  }
84
86
  }
85
87
  catch (error) {
86
- const errorInfo = handleApiError(error, 'Deployment failed. Please check your project files and try again.', {
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(chalk.red(`[Error] ${errorInfo.message}`));
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.default({ ignorecase: false });
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 (process.env.CHABOK_DEBUG === "true") {
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(`No files to archive in ${project_path}. The directory may be empty or all files are ignored.`);
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 = "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.`;
173
208
  return upload_response;
174
209
  }
175
- 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", {
176
212
  total: sourceSize,
177
- width: 20,
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 = `Deployment to service '${selected_service}' finished successfully.`;
227
+ upload_response.message = `Deployed to ${ui.value(selected_service)}.`;
192
228
  }
193
229
  else {
194
- upload_response.message = `Deployment failed: ${String(response.message || "Unknown error occurred. Please try again.")}`;
195
- if (process.env.CHABOK_DEBUG === "true") {
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
  }
@@ -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,16 +1,36 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import inquirer from 'inquirer';
3
3
  import axios from "axios";
4
- import chalk from "chalk";
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 = '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
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 (!flags.token) {
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: 'Enter your username:',
77
+ message: 'Email address:',
29
78
  name: 'username',
30
79
  validate(input) {
31
80
  if (input.length === 0) {
32
- return 'Username cannot be empty';
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: 'Enter your password:',
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(`${chalk.red('[Error]')} Username and password are required!`);
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(`${chalk.green('[Success]')} You are logged in successfully as '${body.username}'.`);
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(`${chalk.red('[Error]')} Login failed: Invalid username or password. Please check your credentials and try again.`);
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, 'Login failed. Please check your credentials and try again.', {
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(`${chalk.red('[Error]')} ${errorInfo.message}`);
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/lib/interfaces/parser.js").BooleanFlag<void>;
9
+ help: import("@oclif/core/interfaces").BooleanFlag<void>;
6
10
  };
7
11
  run(): Promise<void>;
8
12
  }