@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.
@@ -0,0 +1,324 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
+ import { getClient } from '../lib/sdk.js';
3
+ import {
4
+ failure,
5
+ getCliVersion,
6
+ gold,
7
+ green,
8
+ header,
9
+ inactive,
10
+ jsonOutput,
11
+ muted,
12
+ renderTable,
13
+ reset,
14
+ } from '../lib/banner.js';
15
+ import type { CodeInvocation } from '@globio/sdk';
16
+
17
+ const version = getCliVersion();
18
+
19
+ function parseJsonField<T>(value: string | null | undefined): T | null {
20
+ if (!value) return null;
21
+ try {
22
+ return JSON.parse(value) as T;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ const HOOK_TRIGGERS = [
29
+ 'id.onSignup',
30
+ 'id.onSignin',
31
+ 'id.onSignout',
32
+ 'id.onPasswordReset',
33
+ 'doc.onCreate',
34
+ 'doc.onUpdate',
35
+ 'doc.onDelete',
36
+ 'mart.onPurchase',
37
+ 'mart.onPayment',
38
+ 'sync.onRoomCreate',
39
+ 'sync.onRoomClose',
40
+ 'sync.onPlayerJoin',
41
+ 'sync.onPlayerLeave',
42
+ 'vault.onUpload',
43
+ 'vault.onDelete',
44
+ 'signal.onDeliver',
45
+ ] as const;
46
+
47
+ export async function hooksList(
48
+ options: { profile?: string; json?: boolean } = {}
49
+ ) {
50
+ const client = getClient(options.profile);
51
+ const result = await client.code.listHooks();
52
+
53
+ if (options.json) {
54
+ jsonOutput(
55
+ result.success
56
+ ? (result.data ?? []).map((hook) => ({
57
+ slug: hook.slug,
58
+ type: hook.type,
59
+ trigger_event: hook.trigger_event,
60
+ active: hook.active,
61
+ }))
62
+ : []
63
+ );
64
+ }
65
+
66
+ if (!result.success || !result.data?.length) {
67
+ console.log(header(version));
68
+ console.log(' ' + muted('No hooks found.') + '\n');
69
+ return;
70
+ }
71
+
72
+ const rows = result.data.map((fn) => [
73
+ gold(fn.slug),
74
+ gold(fn.trigger_event ?? '—'),
75
+ fn.active ? green('active') : inactive('inactive'),
76
+ ]);
77
+
78
+ console.log(header(version));
79
+ console.log(
80
+ renderTable({
81
+ columns: [
82
+ { header: 'Hook', width: 24 },
83
+ { header: 'Trigger', width: 24 },
84
+ { header: 'Status', width: 10 },
85
+ ],
86
+ rows,
87
+ })
88
+ );
89
+ console.log('');
90
+ }
91
+
92
+ export async function hooksCreate(
93
+ slug: string,
94
+ options: { json?: boolean } = {}
95
+ ) {
96
+ const filename = `${slug}.hook.js`;
97
+ if (existsSync(filename)) {
98
+ if (options.json) {
99
+ jsonOutput({ success: false, file: filename, error: 'File already exists' });
100
+ }
101
+ console.log(gold(filename) + reset + ' already exists.');
102
+ return;
103
+ }
104
+
105
+ const template = `/**
106
+ * GC Hook: ${slug}
107
+ * This hook fires automatically when its trigger event occurs.
108
+ * You cannot invoke it manually.
109
+ *
110
+ * The handler receives the event payload and the injected
111
+ * globio SDK — use it to orchestrate any Globio service.
112
+ */
113
+ async function handler(payload, globio) {
114
+ // payload: event data from the trigger
115
+ // globio: injected SDK — access all Globio services
116
+
117
+ // Example for id.onSignup:
118
+ // const { userId, email } = payload;
119
+ // await globio.doc.set('players', userId, {
120
+ // level: 1, xp: 0, coins: 100
121
+ // });
122
+ }
123
+ `;
124
+
125
+ writeFileSync(filename, template);
126
+
127
+ if (options.json) {
128
+ jsonOutput({ success: true, file: filename });
129
+ }
130
+
131
+ console.log(green('✓') + reset + ' Created ' + gold(filename) + reset);
132
+ console.log(muted(' Deploy with: globio hooks deploy ' + slug));
133
+ }
134
+
135
+ export async function hooksDeploy(
136
+ slug: string,
137
+ options: {
138
+ file?: string;
139
+ name?: string;
140
+ trigger?: string;
141
+ profile?: string;
142
+ json?: boolean;
143
+ }
144
+ ) {
145
+ const filename = options.file ?? `${slug}.hook.js`;
146
+ if (!existsSync(filename)) {
147
+ if (options.json) {
148
+ jsonOutput({ success: false, error: `File not found: ${filename}` });
149
+ }
150
+ console.log(
151
+ failure('File not found: ' + filename) +
152
+ reset +
153
+ ' Run: globio hooks create ' +
154
+ slug
155
+ );
156
+ process.exit(1);
157
+ }
158
+
159
+ if (!options.trigger) {
160
+ if (options.json) {
161
+ jsonOutput({ success: false, error: '--trigger required for hooks' });
162
+ }
163
+ console.log(
164
+ failure('--trigger required for hooks.') +
165
+ reset +
166
+ '\n\n Available triggers:\n' +
167
+ HOOK_TRIGGERS.map((trigger) => ' ' + gold(trigger) + reset).join('\n')
168
+ );
169
+ process.exit(1);
170
+ }
171
+
172
+ const code = readFileSync(filename, 'utf-8');
173
+ const client = getClient(options.profile);
174
+ const existing = await client.code.getFunction(slug).catch(() => null);
175
+
176
+ let result;
177
+ if (existing?.success) {
178
+ result = await client.code.updateHook(slug, {
179
+ code,
180
+ trigger: options.trigger as (typeof HOOK_TRIGGERS)[number],
181
+ });
182
+
183
+ if (options.json) {
184
+ jsonOutput({ success: result.success, slug, action: 'updated' });
185
+ }
186
+
187
+ if (!result.success) {
188
+ console.log(failure('Deploy failed'));
189
+ process.exit(1);
190
+ }
191
+
192
+ console.log(green('✓') + reset + ' Updated hook ' + gold(slug) + reset);
193
+ return;
194
+ }
195
+
196
+ result = await client.code.createHook({
197
+ name: options.name ?? slug,
198
+ slug,
199
+ trigger: options.trigger as (typeof HOOK_TRIGGERS)[number],
200
+ code,
201
+ });
202
+
203
+ if (options.json) {
204
+ jsonOutput({ success: result.success, slug, action: 'created' });
205
+ }
206
+
207
+ if (!result.success) {
208
+ console.log(failure('Deploy failed'));
209
+ process.exit(1);
210
+ }
211
+
212
+ console.log(green('✓') + reset + ' Deployed hook ' + gold(slug) + reset);
213
+ }
214
+
215
+ export async function hooksLogs(
216
+ slug: string,
217
+ options: { limit?: string; profile?: string; json?: boolean } = {}
218
+ ) {
219
+ const limit = options.limit ? parseInt(options.limit, 10) : 20;
220
+ const client = getClient(options.profile);
221
+ const result = await client.code.getHookInvocations(slug, limit);
222
+
223
+ if (options.json) {
224
+ jsonOutput(
225
+ result.success
226
+ ? (result.data as Array<CodeInvocation & {
227
+ logs?: string | null;
228
+ error_message?: string | null;
229
+ input?: string | null;
230
+ result?: string | null;
231
+ }>).map((invocation) => ({
232
+ id: invocation.id,
233
+ trigger_type: invocation.trigger_type,
234
+ duration_ms: invocation.duration_ms,
235
+ success: invocation.success,
236
+ invoked_at: invocation.invoked_at,
237
+ logs: parseJsonField<string[]>(invocation.logs) ?? [],
238
+ error_message: invocation.error_message ?? null,
239
+ input: parseJsonField<Record<string, unknown>>(invocation.input),
240
+ result: parseJsonField<unknown>(invocation.result),
241
+ }))
242
+ : []
243
+ );
244
+ }
245
+
246
+ if (!result.success || !result.data?.length) {
247
+ console.log(header(version));
248
+ console.log(' ' + muted('No invocations yet.') + '\n');
249
+ return;
250
+ }
251
+
252
+ const rows = result.data.map((inv) => {
253
+ const date = new Date(inv.invoked_at * 1000)
254
+ .toISOString()
255
+ .replace('T', ' ')
256
+ .slice(0, 19);
257
+ return [
258
+ muted(date),
259
+ muted(inv.duration_ms + 'ms'),
260
+ inv.success ? green('success') : failure('failed'),
261
+ ];
262
+ });
263
+
264
+ console.log(header(version));
265
+ console.log(
266
+ renderTable({
267
+ columns: [
268
+ { header: 'Time', width: 21 },
269
+ { header: 'Duration', width: 10 },
270
+ { header: 'Status', width: 10 },
271
+ ],
272
+ rows,
273
+ })
274
+ );
275
+ console.log('');
276
+ }
277
+
278
+ export async function hooksToggle(
279
+ slug: string,
280
+ active: boolean,
281
+ options: { profile?: string; json?: boolean } = {}
282
+ ) {
283
+ const client = getClient(options.profile);
284
+ const result = await client.code.toggleHook(slug, active);
285
+
286
+ if (options.json) {
287
+ jsonOutput({ success: result.success, slug, active });
288
+ }
289
+
290
+ if (!result.success) {
291
+ console.log(failure('Toggle failed'));
292
+ process.exit(1);
293
+ }
294
+
295
+ console.log(
296
+ green('✓') +
297
+ reset +
298
+ ' ' +
299
+ gold(slug) +
300
+ reset +
301
+ ' is now ' +
302
+ (active ? green('active') : inactive('inactive')) +
303
+ reset
304
+ );
305
+ }
306
+
307
+ export async function hooksDelete(
308
+ slug: string,
309
+ options: { profile?: string; json?: boolean } = {}
310
+ ) {
311
+ const client = getClient(options.profile);
312
+ const result = await client.code.deleteHook(slug);
313
+
314
+ if (options.json) {
315
+ jsonOutput({ success: result.success, slug });
316
+ }
317
+
318
+ if (!result.success) {
319
+ console.log(failure('Delete failed'));
320
+ process.exit(1);
321
+ }
322
+
323
+ console.log(green('✓') + reset + ' Deleted hook ' + gold(slug) + reset);
324
+ }
@@ -4,6 +4,7 @@ import { config } from '../lib/config.js';
4
4
  import {
5
5
  failure,
6
6
  getCliVersion,
7
+ jsonOutput,
7
8
  muted,
8
9
  orange,
9
10
  printSuccess,
@@ -11,33 +12,83 @@ import {
11
12
  } from '../lib/banner.js';
12
13
  import { promptInit } from '../prompts/init.js';
13
14
  import { migrateFirestore, migrateFirebaseStorage } from './migrate.js';
14
- import { projectsCreate, projectsUse } from './projects.js';
15
+ import { buildSlug, createProjectRecord, projectsCreate, projectsUse } from './projects.js';
15
16
 
16
17
  const version = getCliVersion();
17
18
 
18
- export async function init(options: { profile?: string } = {}) {
19
- printBanner(version);
20
- p.intro(orange('⇒⇒') + ' Initialize your Globio project');
21
-
19
+ export async function init(
20
+ options: {
21
+ profile?: string;
22
+ name?: string;
23
+ slug?: string;
24
+ org?: string;
25
+ env?: string;
26
+ migrate?: boolean;
27
+ from?: string;
28
+ json?: boolean;
29
+ } = {}
30
+ ) {
22
31
  const profileName = options.profile ?? config.getActiveProfile() ?? 'default';
23
32
  const profile = config.getProfile(profileName);
24
33
  if (!profile) {
34
+ if (options.json) {
35
+ jsonOutput({ success: false, error: `Run: npx @globio/cli login --profile ${profileName}` });
36
+ }
25
37
  console.log(failure('Run: npx @globio/cli login --profile ' + profileName));
26
38
  process.exit(1);
27
39
  }
28
40
 
29
- if (!profile.active_project_id) {
30
- await projectsCreate({ profile: profileName });
41
+ const isNonInteractive = Boolean(options.name && options.org);
42
+ let filesCreated: string[] = [];
43
+
44
+ if (options.json && !isNonInteractive) {
45
+ jsonOutput({
46
+ success: false,
47
+ error: 'init --json requires --name <name> and --org <orgId>',
48
+ });
49
+ }
50
+
51
+ if (!isNonInteractive) {
52
+ printBanner(version);
53
+ p.intro(orange('⇒⇒') + ' Initialize your Globio project');
54
+
55
+ if (!profile.active_project_id) {
56
+ await projectsCreate({ profile: profileName });
57
+ } else {
58
+ await projectsUse(profile.active_project_id, { profile: profileName });
59
+ }
31
60
  } else {
32
- await projectsUse(profile.active_project_id, { profile: profileName });
61
+ const orgId = options.org as string;
62
+ const orgName = profile.org_name;
63
+ const created = await createProjectRecord({
64
+ profileName,
65
+ orgId,
66
+ orgName,
67
+ name: options.name as string,
68
+ slug: options.slug ?? buildSlug(options.name as string),
69
+ environment: options.env ?? 'development',
70
+ });
71
+ void created;
33
72
  }
34
73
 
35
- const values = await promptInit();
74
+ const values = isNonInteractive
75
+ ? {
76
+ migrateFromFirebase: Boolean(options.from),
77
+ serviceAccountPath: options.from,
78
+ }
79
+ : await promptInit();
36
80
  const activeProfile = config.getProfile(profileName);
37
81
  const activeProjectKey = activeProfile?.project_api_key;
38
82
  const { projectId: activeProjectId } = config.requireProject(profileName);
83
+ const activeProjectName = activeProfile?.active_project_name ?? 'unnamed';
39
84
 
40
85
  if (!activeProjectKey) {
86
+ if (options.json) {
87
+ jsonOutput({
88
+ success: false,
89
+ error: `No project API key cached. Run: npx @globio/cli projects use ${activeProjectId}`,
90
+ });
91
+ }
41
92
  console.log(failure('No project API key cached. Run: npx @globio/cli projects use ' + activeProjectId));
42
93
  process.exit(1);
43
94
  }
@@ -53,17 +104,25 @@ export const globio = new Globio({
53
104
  });
54
105
  `
55
106
  );
56
- printSuccess('Created globio.config.ts');
107
+ filesCreated.push('globio.config.ts');
108
+ if (!options.json) {
109
+ printSuccess('Created globio.config.ts');
110
+ }
57
111
  }
58
112
 
59
113
  if (!existsSync('.env')) {
60
114
  writeFileSync('.env', `GLOBIO_API_KEY=${activeProjectKey}\n`);
61
- printSuccess('Created .env');
115
+ filesCreated.push('.env');
116
+ if (!options.json) {
117
+ printSuccess('Created .env');
118
+ }
62
119
  }
63
120
 
64
121
  if (values.migrateFromFirebase && values.serviceAccountPath) {
65
- console.log('');
66
- printSuccess('Starting Firebase migration...');
122
+ if (!options.json) {
123
+ console.log('');
124
+ printSuccess('Starting Firebase migration...');
125
+ }
67
126
 
68
127
  await migrateFirestore({
69
128
  from: values.serviceAccountPath as string,
@@ -83,6 +142,16 @@ export const globio = new Globio({
83
142
  });
84
143
  }
85
144
 
145
+ if (options.json) {
146
+ jsonOutput({
147
+ success: true,
148
+ project_id: activeProjectId,
149
+ project_name: activeProjectName,
150
+ api_key: activeProjectKey,
151
+ files_created: filesCreated,
152
+ });
153
+ }
154
+
86
155
  console.log('');
87
156
  p.outro(
88
157
  orange('⇒⇒') +