@globio/cli 0.2.0 → 0.2.3

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.2.0",
3
+ "version": "0.2.3",
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.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "The official CLI for Globio — game backend as a service",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@clack/prompts": "^0.9.0",
17
- "@globio/sdk": "^1.0.0",
17
+ "@globio/sdk": "^1.1.0",
18
18
  "chalk": "^5.3.0",
19
19
  "cli-progress": "^3.12.0",
20
20
  "commander": "^12.0.0",
package/src/auth/login.ts CHANGED
@@ -2,7 +2,7 @@ import * as p from '@clack/prompts';
2
2
  import { exec } from 'child_process';
3
3
  import { config } from '../lib/config.js';
4
4
  import { getConsoleCliAuthUrl, manageRequest, type ManageAccount } from '../lib/manage.js';
5
- import { failure, getCliVersion, muted, orange, printBanner, white } from '../lib/banner.js';
5
+ import { failure, getCliVersion, jsonOutput, muted, orange, printBanner, white } from '../lib/banner.js';
6
6
 
7
7
  const version = getCliVersion();
8
8
 
@@ -25,6 +25,19 @@ async function savePat(token: string) {
25
25
  return account;
26
26
  }
27
27
 
28
+ function storeProfile(profileName: string, token: string, account: ManageAccount) {
29
+ const hadProfiles = config.listProfiles().length > 0;
30
+ config.setProfile(profileName, {
31
+ pat: token,
32
+ account_email: account.email,
33
+ account_name: account.display_name ?? account.email,
34
+ created_at: Date.now(),
35
+ });
36
+ if (profileName === 'default' || !hadProfiles) {
37
+ config.setActiveProfile(profileName);
38
+ }
39
+ }
40
+
28
41
  function warnOnDuplicateAccount(accountEmail: string, targetProfileName: string) {
29
42
  const allProfiles = config.listProfiles();
30
43
  const duplicate = allProfiles.find((name) => {
@@ -45,8 +58,30 @@ function warnOnDuplicateAccount(accountEmail: string, targetProfileName: string)
45
58
  console.log('');
46
59
  }
47
60
 
48
- async function runTokenLogin(profileName: string) {
49
- const hadProfiles = config.listProfiles().length > 0;
61
+ async function completeTokenLogin(
62
+ token: string,
63
+ profileName: string,
64
+ json = false
65
+ ) {
66
+ const account = await savePat(token);
67
+ if (!json) {
68
+ warnOnDuplicateAccount(account.email, profileName);
69
+ }
70
+ storeProfile(profileName, token, account);
71
+
72
+ if (json) {
73
+ jsonOutput({
74
+ success: true,
75
+ email: account.email,
76
+ name: account.display_name ?? account.email,
77
+ profile: profileName,
78
+ });
79
+ }
80
+
81
+ return account;
82
+ }
83
+
84
+ async function runTokenLogin(profileName: string, json = false) {
50
85
  const token = await p.text({
51
86
  message: 'Paste your personal access token',
52
87
  placeholder: 'glo_pat_...',
@@ -65,17 +100,7 @@ async function runTokenLogin(profileName: string) {
65
100
  const spinner = p.spinner();
66
101
  spinner.start('Validating personal access token...');
67
102
  try {
68
- const account = await savePat(token);
69
- warnOnDuplicateAccount(account.email, profileName);
70
- config.setProfile(profileName, {
71
- pat: token,
72
- account_email: account.email,
73
- account_name: account.display_name ?? account.email,
74
- created_at: Date.now(),
75
- });
76
- if (profileName === 'default' || !hadProfiles) {
77
- config.setActiveProfile(profileName);
78
- }
103
+ const account = await completeTokenLogin(token, profileName, json);
79
104
  spinner.stop('Token validated.');
80
105
  p.outro(`Logged in as ${account.email}\nProfile: ${profileName}`);
81
106
  } catch (error) {
@@ -85,10 +110,9 @@ async function runTokenLogin(profileName: string) {
85
110
  }
86
111
  }
87
112
 
88
- async function runBrowserLogin(profileName: string) {
113
+ async function runBrowserLogin(profileName: string, json = false) {
89
114
  const state = crypto.randomUUID();
90
115
  const spinner = p.spinner();
91
- const hadProfiles = config.listProfiles().length > 0;
92
116
 
93
117
  await manageRequest('/cli-auth/request', {
94
118
  method: 'POST',
@@ -124,17 +148,28 @@ async function runBrowserLogin(profileName: string) {
124
148
  body: { code: status.code },
125
149
  });
126
150
 
127
- warnOnDuplicateAccount(exchange.account.email, profileName);
151
+ if (!json) {
152
+ warnOnDuplicateAccount(exchange.account.email, profileName);
153
+ }
128
154
  config.setProfile(profileName, {
129
155
  pat: exchange.token,
130
156
  account_email: exchange.account.email,
131
157
  account_name: exchange.account.display_name ?? exchange.account.email,
132
158
  created_at: Date.now(),
133
159
  });
134
- if (profileName === 'default' || !hadProfiles) {
160
+ if (profileName === 'default' || config.listProfiles().length === 1) {
135
161
  config.setActiveProfile(profileName);
136
162
  }
137
163
 
164
+ if (json) {
165
+ jsonOutput({
166
+ success: true,
167
+ email: exchange.account.email,
168
+ name: exchange.account.display_name ?? exchange.account.email,
169
+ profile: profileName,
170
+ });
171
+ }
172
+
138
173
  spinner.stop('Browser approval received.');
139
174
  p.outro(`Logged in as ${exchange.account.email}\nProfile: ${profileName}`);
140
175
  return;
@@ -151,11 +186,40 @@ async function runBrowserLogin(profileName: string) {
151
186
  process.exit(1);
152
187
  }
153
188
 
154
- export async function login(options: { token?: boolean; profile?: string } = {}) {
155
- printBanner(version);
189
+ export async function login(
190
+ options: { token?: string; profile?: string; json?: boolean } = {}
191
+ ) {
156
192
  const profileName = options.profile ?? 'default';
157
193
  const existing = config.getProfile(profileName);
158
194
 
195
+ if (options.token) {
196
+ try {
197
+ const account = await completeTokenLogin(options.token, profileName, options.json);
198
+ if (!options.json) {
199
+ console.log(`Logged in as ${account.email}\nProfile: ${profileName}`);
200
+ }
201
+ return;
202
+ } catch (error) {
203
+ if (options.json) {
204
+ jsonOutput({
205
+ success: false,
206
+ error: error instanceof Error ? error.message : 'Could not validate token',
207
+ });
208
+ }
209
+ console.log(failure(error instanceof Error ? error.message : 'Could not validate token'));
210
+ process.exit(1);
211
+ }
212
+ }
213
+
214
+ if (options.json) {
215
+ jsonOutput({
216
+ success: false,
217
+ error: 'login --json requires --token <pat>',
218
+ });
219
+ }
220
+
221
+ printBanner(version);
222
+
159
223
  if (existing) {
160
224
  const proceed = await p.confirm({
161
225
  message: `Already logged in as ${existing.account_email} on profile "${profileName}". Replace?`,
@@ -168,11 +232,6 @@ export async function login(options: { token?: boolean; profile?: string } = {})
168
232
  }
169
233
  }
170
234
 
171
- if (options.token) {
172
- await runTokenLogin(profileName);
173
- return;
174
- }
175
-
176
235
  const choice = await p.select({
177
236
  message: 'Choose a login method',
178
237
  options: [
@@ -187,12 +246,12 @@ export async function login(options: { token?: boolean; profile?: string } = {})
187
246
  }
188
247
 
189
248
  if (choice === 'token') {
190
- await runTokenLogin(profileName);
249
+ await runTokenLogin(profileName, options.json);
191
250
  return;
192
251
  }
193
252
 
194
253
  try {
195
- await runBrowserLogin(profileName);
254
+ await runBrowserLogin(profileName, options.json);
196
255
  } catch (error) {
197
256
  p.outro(failure(error instanceof Error ? error.message : 'Could not connect to Globio.') + '\x1b[0m');
198
257
  process.exit(1);
@@ -4,6 +4,7 @@ import {
4
4
  getCliVersion,
5
5
  header,
6
6
  inactive,
7
+ jsonOutput,
7
8
  muted,
8
9
  orange,
9
10
  renderTable,
@@ -14,11 +15,24 @@ import {
14
15
 
15
16
  const version = getCliVersion();
16
17
 
17
- export async function whoami(options: { profile?: string } = {}) {
18
+ export async function whoami(options: { profile?: string; json?: boolean } = {}) {
18
19
  const profileName = options.profile ?? config.getActiveProfile() ?? 'default';
19
20
  const profile = config.getProfile(profileName);
21
+ const active = config.getActiveProfile();
20
22
 
21
23
  if (!profile) {
24
+ if (options.json) {
25
+ jsonOutput({
26
+ profile: profileName,
27
+ active: false,
28
+ account_email: null,
29
+ account_name: null,
30
+ org_name: null,
31
+ active_project_id: null,
32
+ active_project_name: null,
33
+ });
34
+ }
35
+
22
36
  console.log(
23
37
  header(version) +
24
38
  ' ' +
@@ -29,8 +43,19 @@ export async function whoami(options: { profile?: string } = {}) {
29
43
  return;
30
44
  }
31
45
 
46
+ if (options.json) {
47
+ jsonOutput({
48
+ profile: profileName,
49
+ active: profileName === active,
50
+ account_email: profile.account_email,
51
+ account_name: profile.account_name,
52
+ org_name: profile.org_name ?? null,
53
+ active_project_id: profile.active_project_id ?? null,
54
+ active_project_name: profile.active_project_name ?? null,
55
+ });
56
+ }
57
+
32
58
  const allProfiles = config.listProfiles();
33
- const active = config.getActiveProfile();
34
59
  const otherProfiles = allProfiles.filter((p) => p !== profileName).join(', ') || '—';
35
60
 
36
61
  console.log(
@@ -8,6 +8,7 @@ import {
8
8
  green,
9
9
  header,
10
10
  inactive,
11
+ jsonOutput,
11
12
  muted,
12
13
  orange,
13
14
  gold,
@@ -21,11 +22,33 @@ function resolveProfileName(profile?: string) {
21
22
  return profile ?? config.getActiveProfile() ?? 'default';
22
23
  }
23
24
 
24
- export async function functionsList(options: { profile?: string } = {}) {
25
+ function parseJsonField<T>(value: string | null | undefined): T | null {
26
+ if (!value) return null;
27
+ try {
28
+ return JSON.parse(value) as T;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ export async function functionsList(options: { profile?: string; json?: boolean } = {}) {
25
35
  const profileName = resolveProfileName(options.profile);
26
36
  const client = getClient(profileName);
27
37
  const result = await client.code.listFunctions();
28
38
 
39
+ if (options.json) {
40
+ jsonOutput(
41
+ result.success
42
+ ? result.data.map((fn: CodeFunction) => ({
43
+ slug: fn.slug,
44
+ type: fn.type,
45
+ trigger_event: fn.trigger_event,
46
+ active: fn.active,
47
+ }))
48
+ : []
49
+ );
50
+ }
51
+
29
52
  if (!result.success || !result.data.length) {
30
53
  console.log(header(version) + ' ' + muted('No functions found.') + '\n');
31
54
  return;
@@ -53,9 +76,15 @@ export async function functionsList(options: { profile?: string } = {}) {
53
76
  console.log('');
54
77
  }
55
78
 
56
- export async function functionsCreate(slug: string, _options: { profile?: string } = {}) {
79
+ export async function functionsCreate(
80
+ slug: string,
81
+ options: { profile?: string; json?: boolean } = {}
82
+ ) {
57
83
  const filename = `${slug}.js`;
58
84
  if (existsSync(filename)) {
85
+ if (options.json) {
86
+ jsonOutput({ success: false, file: filename, error: 'File already exists' });
87
+ }
59
88
  console.log(inactive(`${filename} already exists.`));
60
89
  return;
61
90
  }
@@ -76,13 +105,16 @@ async function handler(input, globio) {
76
105
  }
77
106
  `;
78
107
  writeFileSync(filename, template);
108
+ if (options.json) {
109
+ jsonOutput({ success: true, file: filename });
110
+ }
79
111
  console.log(green(`Created ${filename}`));
80
112
  console.log(muted(`Deploy with: npx @globio/cli functions deploy ${slug}`));
81
113
  }
82
114
 
83
115
  export async function functionsDeploy(
84
116
  slug: string,
85
- options: { file?: string; name?: string; profile?: string }
117
+ options: { file?: string; name?: string; profile?: string; json?: boolean }
86
118
  ) {
87
119
  const filename = options.file ?? `${slug}.js`;
88
120
  if (!existsSync(filename)) {
@@ -97,8 +129,8 @@ export async function functionsDeploy(
97
129
  const code = readFileSync(filename, 'utf-8');
98
130
  const profileName = resolveProfileName(options.profile);
99
131
  const client = getClient(profileName);
100
- const spinner = ora(`Deploying ${slug}...`).start();
101
132
  const existing = await client.code.getFunction(slug);
133
+ const spinner = options.json ? null : ora(`Deploying ${slug}...`).start();
102
134
 
103
135
  let result;
104
136
  if (existing.success) {
@@ -116,17 +148,28 @@ export async function functionsDeploy(
116
148
  }
117
149
 
118
150
  if (!result.success) {
119
- spinner.fail('Deploy failed');
151
+ if (options.json) {
152
+ jsonOutput({ success: false, error: result.error.message });
153
+ }
154
+ spinner?.fail('Deploy failed');
120
155
  console.error(result.error.message);
121
156
  process.exit(1);
122
157
  }
123
158
 
124
- spinner.succeed(existing.success ? `Updated ${slug}` : `Deployed ${slug}`);
159
+ if (options.json) {
160
+ jsonOutput({
161
+ success: true,
162
+ slug,
163
+ action: existing.success ? 'updated' : 'created',
164
+ });
165
+ }
166
+
167
+ spinner?.succeed(existing.success ? `Updated ${slug}` : `Deployed ${slug}`);
125
168
  }
126
169
 
127
170
  export async function functionsInvoke(
128
171
  slug: string,
129
- options: { input?: string; profile?: string }
172
+ options: { input?: string; profile?: string; json?: boolean }
130
173
  ) {
131
174
  let input: Record<string, unknown> = {};
132
175
  if (options.input) {
@@ -140,16 +183,26 @@ export async function functionsInvoke(
140
183
 
141
184
  const profileName = resolveProfileName(options.profile);
142
185
  const client = getClient(profileName);
143
- const spinner = ora(`Invoking ${slug}...`).start();
186
+ const spinner = options.json ? null : ora(`Invoking ${slug}...`).start();
144
187
  const result = await client.code.invoke(slug, input);
145
- spinner.stop();
188
+ spinner?.stop();
146
189
 
147
190
  if (!result.success) {
191
+ if (options.json) {
192
+ jsonOutput({ success: false, error: result.error.message });
193
+ }
148
194
  console.log(failure('Invocation failed'));
149
195
  console.error(result.error.message);
150
196
  return;
151
197
  }
152
198
 
199
+ if (options.json) {
200
+ jsonOutput({
201
+ result: result.data.result,
202
+ duration_ms: result.data.duration_ms,
203
+ });
204
+ }
205
+
153
206
  console.log('');
154
207
  console.log(orange('Result:'));
155
208
  console.log(JSON.stringify(result.data.result, null, 2));
@@ -158,13 +211,36 @@ export async function functionsInvoke(
158
211
 
159
212
  export async function functionsLogs(
160
213
  slug: string,
161
- options: { limit?: string; profile?: string }
214
+ options: { limit?: string; profile?: string; json?: boolean }
162
215
  ) {
163
216
  const limit = options.limit ? parseInt(options.limit, 10) : 20;
164
217
  const profileName = resolveProfileName(options.profile);
165
218
  const client = getClient(profileName);
166
219
  const result = await client.code.getInvocations(slug, limit);
167
220
 
221
+ if (options.json) {
222
+ jsonOutput(
223
+ result.success
224
+ ? (result.data as Array<CodeInvocation & {
225
+ logs?: string | null;
226
+ error_message?: string | null;
227
+ input?: string | null;
228
+ result?: string | null;
229
+ }>).map((invocation) => ({
230
+ id: invocation.id,
231
+ trigger_type: invocation.trigger_type,
232
+ duration_ms: invocation.duration_ms,
233
+ success: invocation.success,
234
+ invoked_at: invocation.invoked_at,
235
+ logs: parseJsonField<string[]>(invocation.logs) ?? [],
236
+ error_message: invocation.error_message ?? null,
237
+ input: parseJsonField<Record<string, unknown>>(invocation.input),
238
+ result: parseJsonField<unknown>(invocation.result),
239
+ }))
240
+ : []
241
+ );
242
+ }
243
+
168
244
  if (!result.success || !result.data.length) {
169
245
  console.log(header(version) + ' ' + muted('No invocations yet.') + '\n');
170
246
  return;
@@ -198,34 +274,49 @@ export async function functionsLogs(
198
274
  console.log('');
199
275
  }
200
276
 
201
- export async function functionsDelete(slug: string, options: { profile?: string } = {}) {
277
+ export async function functionsDelete(
278
+ slug: string,
279
+ options: { profile?: string; json?: boolean } = {}
280
+ ) {
202
281
  const profileName = resolveProfileName(options.profile);
203
282
  const client = getClient(profileName);
204
- const spinner = ora(`Deleting ${slug}...`).start();
283
+ const spinner = options.json ? null : ora(`Deleting ${slug}...`).start();
205
284
  const result = await client.code.deleteFunction(slug);
206
285
  if (!result.success) {
207
- spinner.fail(`Delete failed for ${slug}`);
286
+ if (options.json) {
287
+ jsonOutput({ success: false, error: result.error.message });
288
+ }
289
+ spinner?.fail(`Delete failed for ${slug}`);
208
290
  console.error(result.error.message);
209
291
  process.exit(1);
210
292
  }
211
- spinner.succeed(`Deleted ${slug}`);
293
+ if (options.json) {
294
+ jsonOutput({ success: true, slug });
295
+ }
296
+ spinner?.succeed(`Deleted ${slug}`);
212
297
  }
213
298
 
214
299
  export async function functionsToggle(
215
300
  slug: string,
216
301
  active: boolean,
217
- options: { profile?: string } = {}
302
+ options: { profile?: string; json?: boolean } = {}
218
303
  ) {
219
304
  const profileName = resolveProfileName(options.profile);
220
305
  const client = getClient(profileName);
221
- const spinner = ora(
222
- `${active ? 'Enabling' : 'Disabling'} ${slug}...`
223
- ).start();
306
+ const spinner = options.json
307
+ ? null
308
+ : ora(`${active ? 'Enabling' : 'Disabling'} ${slug}...`).start();
224
309
  const result = await client.code.toggleFunction(slug, active);
225
310
  if (!result.success) {
226
- spinner.fail(`Toggle failed for ${slug}`);
311
+ if (options.json) {
312
+ jsonOutput({ success: false, error: result.error.message });
313
+ }
314
+ spinner?.fail(`Toggle failed for ${slug}`);
227
315
  console.error(result.error.message);
228
316
  process.exit(1);
229
317
  }
230
- spinner.succeed(`${slug} is now ${active ? 'active' : 'inactive'}`);
318
+ if (options.json) {
319
+ jsonOutput({ success: true, slug, active });
320
+ }
321
+ spinner?.succeed(`${slug} is now ${active ? 'active' : 'inactive'}`);
231
322
  }