@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/dist/index.js +702 -144
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/auth/login.ts +86 -27
- package/src/auth/whoami.ts +27 -2
- package/src/commands/functions.ts +111 -20
- package/src/commands/hooks.ts +324 -0
- package/src/commands/init.ts +82 -13
- package/src/commands/migrate.ts +105 -54
- package/src/commands/profiles.ts +16 -1
- package/src/commands/projects.ts +141 -24
- package/src/commands/services.ts +11 -1
- package/src/index.ts +119 -13
- package/src/lib/table.ts +5 -0
|
@@ -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
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -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(
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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('⇒⇒') +
|