@globio/cli 0.1.3 → 0.1.5

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/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globio/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "license": "MIT",
5
5
  "exports": "./src/index.ts"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globio/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "The official CLI for Globio — game backend as a service",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/auth/login.ts CHANGED
@@ -23,15 +23,31 @@ function sleep(ms: number) {
23
23
 
24
24
  async function savePat(token: string) {
25
25
  const account = await manageRequest<ManageAccount>('/account', { token });
26
- config.set({
27
- pat: token,
28
- accountEmail: account.email,
29
- accountName: account.display_name ?? account.email,
30
- });
31
26
  return account;
32
27
  }
33
28
 
34
- async function runTokenLogin() {
29
+ function warnOnDuplicateAccount(accountEmail: string, targetProfileName: string) {
30
+ const allProfiles = config.listProfiles();
31
+ const duplicate = allProfiles.find((name) => {
32
+ const profile = config.getProfile(name);
33
+ return profile?.account_email === accountEmail && name !== targetProfileName;
34
+ });
35
+
36
+ if (!duplicate) return;
37
+
38
+ console.log('');
39
+ console.log(
40
+ chalk.yellow(' ⚠ ') +
41
+ chalk.white(accountEmail) +
42
+ chalk.gray(' is already logged in under profile ') +
43
+ orange(`"${duplicate}"`) +
44
+ chalk.gray('.')
45
+ );
46
+ console.log('');
47
+ }
48
+
49
+ async function runTokenLogin(profileName: string) {
50
+ const hadProfiles = config.listProfiles().length > 0;
35
51
  const token = await p.text({
36
52
  message: 'Paste your personal access token',
37
53
  placeholder: 'glo_pat_...',
@@ -51,8 +67,18 @@ async function runTokenLogin() {
51
67
  spinner.start('Validating personal access token...');
52
68
  try {
53
69
  const account = await savePat(token);
70
+ warnOnDuplicateAccount(account.email, profileName);
71
+ config.setProfile(profileName, {
72
+ pat: token,
73
+ account_email: account.email,
74
+ account_name: account.display_name ?? account.email,
75
+ created_at: Date.now(),
76
+ });
77
+ if (profileName === 'default' || !hadProfiles) {
78
+ config.setActiveProfile(profileName);
79
+ }
54
80
  spinner.stop('Token validated.');
55
- p.outro(`Logged in as ${account.email}`);
81
+ p.outro(`Logged in as ${account.email}\nProfile: ${profileName}`);
56
82
  } catch (error) {
57
83
  spinner.stop('Validation failed.');
58
84
  p.outro(chalk.red(error instanceof Error ? error.message : 'Could not validate token'));
@@ -60,9 +86,10 @@ async function runTokenLogin() {
60
86
  }
61
87
  }
62
88
 
63
- async function runBrowserLogin() {
89
+ async function runBrowserLogin(profileName: string) {
64
90
  const state = crypto.randomUUID();
65
91
  const spinner = p.spinner();
92
+ const hadProfiles = config.listProfiles().length > 0;
66
93
 
67
94
  await manageRequest('/cli-auth/request', {
68
95
  method: 'POST',
@@ -98,14 +125,19 @@ async function runBrowserLogin() {
98
125
  body: { code: status.code },
99
126
  });
100
127
 
101
- config.set({
128
+ warnOnDuplicateAccount(exchange.account.email, profileName);
129
+ config.setProfile(profileName, {
102
130
  pat: exchange.token,
103
- accountEmail: exchange.account.email,
104
- accountName: exchange.account.display_name ?? exchange.account.email,
131
+ account_email: exchange.account.email,
132
+ account_name: exchange.account.display_name ?? exchange.account.email,
133
+ created_at: Date.now(),
105
134
  });
135
+ if (profileName === 'default' || !hadProfiles) {
136
+ config.setActiveProfile(profileName);
137
+ }
106
138
 
107
139
  spinner.stop('Browser approval received.');
108
- p.outro(`Logged in as ${exchange.account.email}`);
140
+ p.outro(`Logged in as ${exchange.account.email}\nProfile: ${profileName}`);
109
141
  return;
110
142
  }
111
143
  } catch {
@@ -120,11 +152,25 @@ async function runBrowserLogin() {
120
152
  process.exit(1);
121
153
  }
122
154
 
123
- export async function login(options: { token?: boolean } = {}) {
155
+ export async function login(options: { token?: boolean; profile?: string } = {}) {
124
156
  printBanner(version);
157
+ const profileName = options.profile ?? 'default';
158
+ const existing = config.getProfile(profileName);
159
+
160
+ if (existing) {
161
+ const proceed = await p.confirm({
162
+ message: `Already logged in as ${existing.account_email} on profile "${profileName}". Replace?`,
163
+ initialValue: false,
164
+ });
165
+
166
+ if (p.isCancel(proceed) || !proceed) {
167
+ p.outro('Login cancelled.');
168
+ return;
169
+ }
170
+ }
125
171
 
126
172
  if (options.token) {
127
- await runTokenLogin();
173
+ await runTokenLogin(profileName);
128
174
  return;
129
175
  }
130
176
 
@@ -142,12 +188,12 @@ export async function login(options: { token?: boolean } = {}) {
142
188
  }
143
189
 
144
190
  if (choice === 'token') {
145
- await runTokenLogin();
191
+ await runTokenLogin(profileName);
146
192
  return;
147
193
  }
148
194
 
149
195
  try {
150
- await runBrowserLogin();
196
+ await runBrowserLogin(profileName);
151
197
  } catch (error) {
152
198
  p.outro(chalk.red(error instanceof Error ? error.message : 'Could not connect to Globio.'));
153
199
  process.exit(1);
@@ -1,8 +1,30 @@
1
- import * as p from '@clack/prompts';
2
1
  import chalk from 'chalk';
3
2
  import { config } from '../lib/config.js';
4
3
 
5
- export async function logout() {
6
- config.clear();
7
- p.outro(chalk.green('Logged out.'));
4
+ export async function logout(options: { profile?: string } = {}) {
5
+ const activeProfile = config.getActiveProfile();
6
+ const profileName = options.profile ?? activeProfile;
7
+ const profile = profileName ? config.getProfile(profileName) : null;
8
+
9
+ if (!profileName || !profile) {
10
+ console.log(chalk.yellow(`No active session on profile "${profileName || 'default'}".`));
11
+ return;
12
+ }
13
+
14
+ config.deleteProfile(profileName);
15
+
16
+ if (profileName === activeProfile) {
17
+ const remaining = config.listProfiles();
18
+ if (remaining.length > 0) {
19
+ config.setActiveProfile(remaining[0]);
20
+ console.log(chalk.green(`Logged out. Switched to profile: ${remaining[0]}`));
21
+ return;
22
+ }
23
+
24
+ config.setActiveProfile('');
25
+ console.log(chalk.green('Logged out.'));
26
+ return;
27
+ }
28
+
29
+ console.log(chalk.green(`Logged out profile: ${profileName}`));
8
30
  }
@@ -0,0 +1,20 @@
1
+ import chalk from 'chalk';
2
+ import { orange } from '../lib/banner.js';
3
+ import { config } from '../lib/config.js';
4
+
5
+ export async function useProfile(profileName: string) {
6
+ const profile = config.getProfile(profileName);
7
+ if (!profile) {
8
+ console.log(
9
+ chalk.red(
10
+ `Profile "${profileName}" not found. Run: globio login --profile ${profileName}`
11
+ )
12
+ );
13
+ process.exit(1);
14
+ }
15
+
16
+ config.setActiveProfile(profileName);
17
+ console.log(
18
+ chalk.green('Switched to profile: ') + orange(profileName) + ` (${profile.account_email})`
19
+ );
20
+ }
@@ -1,19 +1,37 @@
1
1
  import chalk from 'chalk';
2
2
  import { config } from '../lib/config.js';
3
+ import { muted, orange } from '../lib/banner.js';
3
4
 
4
- export async function whoami() {
5
- const cfg = config.get();
6
- if (!cfg.pat) {
7
- console.log(chalk.red('Not logged in.'));
5
+ export async function whoami(options: { profile?: string } = {}) {
6
+ const profileName = options.profile ?? config.getActiveProfile() ?? 'default';
7
+ const profile = config.getProfile(profileName);
8
+
9
+ if (!profile) {
10
+ console.log(chalk.red('Not logged in. Run: globio login'));
8
11
  return;
9
12
  }
10
13
 
14
+ const allProfiles = config.listProfiles();
15
+ const activeProfile = config.getActiveProfile();
16
+
11
17
  console.log('');
12
- console.log(chalk.cyan('Account: ') + (cfg.accountEmail ?? 'unknown'));
13
- console.log(chalk.cyan('Name: ') + (cfg.accountName ?? 'unknown'));
14
18
  console.log(
15
- chalk.cyan('Project: ') +
16
- (cfg.projectId ? `${cfg.projectName ?? 'unnamed'} (${cfg.projectId})` : 'none')
19
+ muted('Profile: ') + orange(profileName) + (profileName === activeProfile ? muted(' (active)') : '')
20
+ );
21
+ console.log(muted('Account: ') + profile.account_email);
22
+ console.log(muted('Name: ') + (profile.account_name || '—'));
23
+ console.log(
24
+ muted('Project: ') +
25
+ (profile.active_project_id
26
+ ? orange(profile.active_project_name || 'unnamed') + muted(` (${profile.active_project_id})`)
27
+ : chalk.gray('none — run: globio projects use <id>'))
17
28
  );
29
+
30
+ if (allProfiles.length > 1) {
31
+ console.log('');
32
+ console.log(
33
+ muted('Other profiles: ') + allProfiles.filter((name) => name !== profileName).join(', ')
34
+ );
35
+ }
18
36
  console.log('');
19
37
  }
@@ -2,11 +2,17 @@ import chalk from 'chalk';
2
2
  import type { CodeFunction, CodeInvocation } from '@globio/sdk';
3
3
  import ora from 'ora';
4
4
  import { existsSync, readFileSync, writeFileSync } from 'fs';
5
+ import { config } from '../lib/config.js';
5
6
  import { gold, muted, orange } from '../lib/banner.js';
6
7
  import { getClient } from '../lib/sdk.js';
7
8
 
8
- export async function functionsList() {
9
- const client = getClient();
9
+ function resolveProfileName(profile?: string) {
10
+ return profile ?? config.getActiveProfile() ?? 'default';
11
+ }
12
+
13
+ export async function functionsList(options: { profile?: string } = {}) {
14
+ const profileName = resolveProfileName(options.profile);
15
+ const client = getClient(profileName);
10
16
  const spinner = ora('Fetching functions...').start();
11
17
  const result = await client.code.listFunctions();
12
18
  spinner.stop();
@@ -29,7 +35,7 @@ export async function functionsList() {
29
35
  console.log('');
30
36
  }
31
37
 
32
- export async function functionsCreate(slug: string) {
38
+ export async function functionsCreate(slug: string, _options: { profile?: string } = {}) {
33
39
  const filename = `${slug}.js`;
34
40
  if (existsSync(filename)) {
35
41
  console.log(chalk.yellow(`${filename} already exists.`));
@@ -60,7 +66,7 @@ async function handler(input, globio) {
60
66
 
61
67
  export async function functionsDeploy(
62
68
  slug: string,
63
- options: { file?: string; name?: string }
69
+ options: { file?: string; name?: string; profile?: string }
64
70
  ) {
65
71
  const filename = options.file ?? `${slug}.js`;
66
72
  if (!existsSync(filename)) {
@@ -73,7 +79,8 @@ export async function functionsDeploy(
73
79
  }
74
80
 
75
81
  const code = readFileSync(filename, 'utf-8');
76
- const client = getClient();
82
+ const profileName = resolveProfileName(options.profile);
83
+ const client = getClient(profileName);
77
84
  const spinner = ora(`Deploying ${slug}...`).start();
78
85
  const existing = await client.code.getFunction(slug);
79
86
 
@@ -103,7 +110,7 @@ export async function functionsDeploy(
103
110
 
104
111
  export async function functionsInvoke(
105
112
  slug: string,
106
- options: { input?: string }
113
+ options: { input?: string; profile?: string }
107
114
  ) {
108
115
  let input: Record<string, unknown> = {};
109
116
  if (options.input) {
@@ -115,7 +122,8 @@ export async function functionsInvoke(
115
122
  }
116
123
  }
117
124
 
118
- const client = getClient();
125
+ const profileName = resolveProfileName(options.profile);
126
+ const client = getClient(profileName);
119
127
  const spinner = ora(`Invoking ${slug}...`).start();
120
128
  const result = await client.code.invoke(slug, input);
121
129
  spinner.stop();
@@ -134,10 +142,11 @@ export async function functionsInvoke(
134
142
 
135
143
  export async function functionsLogs(
136
144
  slug: string,
137
- options: { limit?: string }
145
+ options: { limit?: string; profile?: string }
138
146
  ) {
139
147
  const limit = options.limit ? parseInt(options.limit, 10) : 20;
140
- const client = getClient();
148
+ const profileName = resolveProfileName(options.profile);
149
+ const client = getClient(profileName);
141
150
  const spinner = ora('Fetching invocations...').start();
142
151
  const result = await client.code.getInvocations(slug, limit);
143
152
  spinner.stop();
@@ -163,8 +172,9 @@ export async function functionsLogs(
163
172
  console.log('');
164
173
  }
165
174
 
166
- export async function functionsDelete(slug: string) {
167
- const client = getClient();
175
+ export async function functionsDelete(slug: string, options: { profile?: string } = {}) {
176
+ const profileName = resolveProfileName(options.profile);
177
+ const client = getClient(profileName);
168
178
  const spinner = ora(`Deleting ${slug}...`).start();
169
179
  const result = await client.code.deleteFunction(slug);
170
180
  if (!result.success) {
@@ -175,8 +185,13 @@ export async function functionsDelete(slug: string) {
175
185
  spinner.succeed(`Deleted ${slug}`);
176
186
  }
177
187
 
178
- export async function functionsToggle(slug: string, active: boolean) {
179
- const client = getClient();
188
+ export async function functionsToggle(
189
+ slug: string,
190
+ active: boolean,
191
+ options: { profile?: string } = {}
192
+ ) {
193
+ const profileName = resolveProfileName(options.profile);
194
+ const client = getClient(profileName);
180
195
  const spinner = ora(
181
196
  `${active ? 'Enabling' : 'Disabling'} ${slug}...`
182
197
  ).start();
@@ -14,20 +14,32 @@ import { projectsCreate, projectsUse } from './projects.js';
14
14
 
15
15
  const version = getCliVersion();
16
16
 
17
- export async function init() {
17
+ export async function init(options: { profile?: string } = {}) {
18
18
  printBanner(version);
19
19
  p.intro(orange('⇒⇒') + ' Initialize your Globio project');
20
20
 
21
- const cfg = config.get();
22
- if (!cfg.projectId) {
23
- await projectsCreate();
21
+ const profileName = options.profile ?? config.getActiveProfile() ?? 'default';
22
+ const profile = config.getProfile(profileName);
23
+ if (!profile) {
24
+ console.log('Run: npx @globio/cli login --profile ' + profileName);
25
+ process.exit(1);
26
+ }
27
+
28
+ if (!profile.active_project_id) {
29
+ await projectsCreate({ profile: profileName });
24
30
  } else {
25
- await projectsUse(cfg.projectId);
31
+ await projectsUse(profile.active_project_id, { profile: profileName });
26
32
  }
27
33
 
28
34
  const values = await promptInit();
29
- const activeProjectKey = config.requireProjectApiKey();
30
- const activeProjectId = config.requireProject();
35
+ const activeProfile = config.getProfile(profileName);
36
+ const activeProjectKey = activeProfile?.project_api_key;
37
+ const { projectId: activeProjectId } = config.requireProject(profileName);
38
+
39
+ if (!activeProjectKey) {
40
+ console.log('No project API key cached. Run: npx @globio/cli projects use ' + activeProjectId);
41
+ process.exit(1);
42
+ }
31
43
 
32
44
  if (!existsSync('globio.config.ts')) {
33
45
  writeFileSync(
@@ -54,6 +66,7 @@ export const globio = new Globio({
54
66
  await migrateFirestore({
55
67
  from: values.serviceAccountPath as string,
56
68
  all: true,
69
+ profile: profileName,
57
70
  });
58
71
 
59
72
  const serviceAccount = JSON.parse(
@@ -64,6 +77,7 @@ export const globio = new Globio({
64
77
  from: values.serviceAccountPath as string,
65
78
  bucket: `${serviceAccount.project_id}.appspot.com`,
66
79
  all: true,
80
+ profile: profileName,
67
81
  });
68
82
  }
69
83
 
@@ -11,6 +11,7 @@ import {
11
11
  import { initFirebase } from '../lib/firebase.js';
12
12
  import { createProgressBar } from '../lib/progress.js';
13
13
  import { getClient } from '../lib/sdk.js';
14
+ import { config } from '../lib/config.js';
14
15
 
15
16
  const version = getCliVersion();
16
17
 
@@ -18,6 +19,7 @@ interface MigrateFirestoreOptions {
18
19
  from: string;
19
20
  collection?: string;
20
21
  all?: boolean;
22
+ profile?: string;
21
23
  }
22
24
 
23
25
  interface MigrateStorageOptions {
@@ -25,6 +27,11 @@ interface MigrateStorageOptions {
25
27
  bucket: string;
26
28
  folder?: string;
27
29
  all?: boolean;
30
+ profile?: string;
31
+ }
32
+
33
+ function resolveProfileName(profile?: string) {
34
+ return profile ?? config.getActiveProfile() ?? 'default';
28
35
  }
29
36
 
30
37
  export async function migrateFirestore(options: MigrateFirestoreOptions) {
@@ -32,7 +39,7 @@ export async function migrateFirestore(options: MigrateFirestoreOptions) {
32
39
  p.intro(gold('⇒⇒') + ' Firebase → Globio Migration');
33
40
 
34
41
  const { firestore } = await initFirebase(options.from);
35
- const client = getClient();
42
+ const client = getClient(resolveProfileName(options.profile));
36
43
 
37
44
  let collections: string[] = [];
38
45
 
@@ -139,7 +146,7 @@ export async function migrateFirebaseStorage(options: MigrateStorageOptions) {
139
146
  p.intro(gold('⇒⇒') + ' Firebase → Globio Migration');
140
147
 
141
148
  const { storage } = await initFirebase(options.from);
142
- const client = getClient();
149
+ const client = getClient(resolveProfileName(options.profile));
143
150
 
144
151
  const bucketName = options.bucket.replace(/^gs:\/\//, '');
145
152
  const bucket = storage.bucket(bucketName);
@@ -0,0 +1,26 @@
1
+ import chalk from 'chalk';
2
+ import { orange, muted } from '../lib/banner.js';
3
+ import { config } from '../lib/config.js';
4
+
5
+ export async function profilesList() {
6
+ const profiles = config.listProfiles();
7
+ const active = config.getActiveProfile();
8
+
9
+ if (!profiles.length) {
10
+ console.log(chalk.gray('No profiles found. Run: globio login'));
11
+ return;
12
+ }
13
+
14
+ console.log('');
15
+ for (const name of profiles) {
16
+ const data = config.getProfile(name);
17
+ const isActive = name === active;
18
+ const bullet = isActive ? orange('●') : chalk.gray('○');
19
+ const label = isActive ? orange(name) : chalk.white(name);
20
+ const email = data?.account_email ? muted(data.account_email) : chalk.gray('unknown');
21
+ const tag = isActive ? muted(' (active)') : '';
22
+
23
+ console.log(` ${bullet} ${label} ${email}${tag}`);
24
+ }
25
+ console.log('');
26
+ }
@@ -17,16 +17,18 @@ function slugify(value: string) {
17
17
  .replace(/-+/g, '-');
18
18
  }
19
19
 
20
- async function ensureProjectKey(projectId: string) {
21
- const existingKey = config.getProjectApiKey(projectId);
22
- if (existingKey) return existingKey;
20
+ function resolveProfileName(profileName?: string) {
21
+ return profileName ?? config.getActiveProfile() ?? 'default';
22
+ }
23
23
 
24
+ async function createServerKey(projectId: string, profileName: string) {
24
25
  const created = await manageRequest<ManageProjectKey>(`/projects/${projectId}/keys`, {
25
26
  method: 'POST',
26
27
  body: {
27
28
  name: 'CLI server key',
28
29
  scope: 'server',
29
30
  },
31
+ profileName,
30
32
  });
31
33
 
32
34
  if (!created.token) {
@@ -36,9 +38,12 @@ async function ensureProjectKey(projectId: string) {
36
38
  return created.token;
37
39
  }
38
40
 
39
- export async function projectsList() {
40
- const projects = await manageRequest<ManageProject[]>('/projects');
41
- const activeProjectId = config.get().projectId;
41
+ export async function projectsList(options: { profile?: string } = {}) {
42
+ const profileName = resolveProfileName(options.profile);
43
+ config.requireAuth(profileName);
44
+
45
+ const projects = await manageRequest<ManageProject[]>('/projects', { profileName });
46
+ const activeProjectId = config.getProfile(profileName)?.active_project_id;
42
47
  const grouped = new Map<string, ManageProject[]>();
43
48
 
44
49
  for (const project of projects) {
@@ -65,21 +70,37 @@ export async function projectsList() {
65
70
  }
66
71
  }
67
72
 
68
- export async function projectsUse(projectId: string) {
69
- const projects = await manageRequest<ManageProject[]>('/projects');
73
+ export async function projectsUse(projectId: string, options: { profile?: string } = {}) {
74
+ const profileName = resolveProfileName(options.profile);
75
+ config.requireAuth(profileName);
76
+
77
+ const projects = await manageRequest<ManageProject[]>('/projects', { profileName });
70
78
  const project = projects.find((item) => item.id === projectId);
71
79
  if (!project) {
72
80
  console.log(chalk.red(`Project not found: ${projectId}`));
73
81
  process.exit(1);
74
82
  }
75
83
 
76
- const apiKey = await ensureProjectKey(projectId);
77
- config.setProjectAuth(projectId, apiKey, project.name);
78
- console.log(chalk.green('Active project set to: ') + chalk.cyan(`${project.name} (${project.id})`));
84
+ await manageRequest<ManageProjectKey[]>(`/projects/${projectId}/keys`, { profileName });
85
+ const apiKey = await createServerKey(projectId, profileName);
86
+
87
+ config.setProfile(profileName, {
88
+ active_project_id: project.id,
89
+ active_project_name: project.name,
90
+ project_api_key: apiKey,
91
+ });
92
+ config.setActiveProfile(profileName);
93
+
94
+ console.log(
95
+ chalk.green('Active project set to: ') + chalk.cyan(`${project.name} (${project.id})`)
96
+ );
79
97
  }
80
98
 
81
- export async function projectsCreate() {
82
- const orgs = await manageRequest<ManageOrg[]>('/orgs');
99
+ export async function projectsCreate(options: { profile?: string } = {}) {
100
+ const profileName = resolveProfileName(options.profile);
101
+ config.requireAuth(profileName);
102
+
103
+ const orgs = await manageRequest<ManageOrg[]>('/orgs', { profileName });
83
104
  if (!orgs.length) {
84
105
  console.log(chalk.red('No organizations found. Create one in the console first.'));
85
106
  process.exit(1);
@@ -141,9 +162,15 @@ export async function projectsCreate() {
141
162
  slug: values.slug,
142
163
  environment: values.environment,
143
164
  },
165
+ profileName,
144
166
  });
145
167
 
146
- config.setProjectAuth(result.project.id, result.keys.server, result.project.name);
168
+ config.setProfile(profileName, {
169
+ active_project_id: result.project.id,
170
+ active_project_name: result.project.name,
171
+ project_api_key: result.keys.server,
172
+ });
173
+ config.setActiveProfile(profileName);
147
174
 
148
175
  console.log('');
149
176
  console.log(chalk.green('Project created successfully.'));
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import { config } from '../lib/config.js';
2
3
 
3
4
  const ALL_SERVICES = [
4
5
  'id',
@@ -13,7 +14,9 @@ const ALL_SERVICES = [
13
14
  'code',
14
15
  ];
15
16
 
16
- export async function servicesList() {
17
+ export async function servicesList(options: { profile?: string } = {}) {
18
+ void options.profile;
19
+ void config;
17
20
  console.log('');
18
21
  console.log(chalk.cyan('Available Globio services:'));
19
22
  ALL_SERVICES.forEach((service) => {