@globio/cli 0.0.1 → 0.1.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.
@@ -0,0 +1,65 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { config } from '../lib/config.js';
4
+
5
+ const DEFAULT_BASE_URL = 'https://api.globio.stanlink.online';
6
+
7
+ export async function login() {
8
+ console.log('');
9
+ p.intro(chalk.bgCyan(chalk.black(' Globio CLI ')));
10
+
11
+ const values = await p.group(
12
+ {
13
+ apiKey: () =>
14
+ p.text({
15
+ message: 'Paste your Globio API key',
16
+ placeholder: 'gk_live_...',
17
+ validate: (value) => (!value ? 'API key is required' : undefined),
18
+ }),
19
+ projectId: () =>
20
+ p.text({
21
+ message: 'Paste your Project ID',
22
+ placeholder: 'proj_...',
23
+ validate: (value) => (!value ? 'Project ID is required' : undefined),
24
+ }),
25
+ },
26
+ {
27
+ onCancel: () => {
28
+ p.cancel('Login cancelled.');
29
+ process.exit(0);
30
+ },
31
+ }
32
+ );
33
+
34
+ const spinner = p.spinner();
35
+ spinner.start('Validating credentials...');
36
+
37
+ try {
38
+ const response = await fetch(`${DEFAULT_BASE_URL}/id/health`, {
39
+ headers: {
40
+ 'X-Globio-Key': values.apiKey as string,
41
+ },
42
+ });
43
+
44
+ if (!response.ok) {
45
+ spinner.stop('Validation failed.');
46
+ p.outro(chalk.red('Invalid API key or project ID.'));
47
+ process.exit(1);
48
+ }
49
+
50
+ config.set({
51
+ apiKey: values.apiKey as string,
52
+ projectId: values.projectId as string,
53
+ });
54
+
55
+ spinner.stop('Credentials validated.');
56
+ p.outro(
57
+ chalk.green('Logged in. Active project: ') +
58
+ chalk.cyan(values.projectId as string)
59
+ );
60
+ } catch {
61
+ spinner.stop('');
62
+ p.outro(chalk.red('Could not connect to Globio. Check your credentials.'));
63
+ process.exit(1);
64
+ }
65
+ }
@@ -0,0 +1,8 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { config } from '../lib/config.js';
4
+
5
+ export async function logout() {
6
+ config.clear();
7
+ p.outro(chalk.green('Logged out.'));
8
+ }
@@ -0,0 +1,15 @@
1
+ import chalk from 'chalk';
2
+ import { config } from '../lib/config.js';
3
+
4
+ export async function whoami() {
5
+ const cfg = config.get();
6
+ if (!cfg.apiKey) {
7
+ console.log(chalk.red('Not logged in.'));
8
+ return;
9
+ }
10
+
11
+ console.log('');
12
+ console.log(chalk.cyan('API Key: ') + cfg.apiKey);
13
+ console.log(chalk.cyan('Project: ') + (cfg.projectId ?? 'none'));
14
+ console.log('');
15
+ }
@@ -0,0 +1,191 @@
1
+ import chalk from 'chalk';
2
+ import type { CodeFunction, CodeInvocation } from '@globio/sdk';
3
+ import ora from 'ora';
4
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
5
+ import { getClient } from '../lib/sdk.js';
6
+
7
+ export async function functionsList() {
8
+ const client = getClient();
9
+ const spinner = ora('Fetching functions...').start();
10
+ const result = await client.code.listFunctions();
11
+ spinner.stop();
12
+
13
+ if (!result.success || !result.data.length) {
14
+ console.log(chalk.gray('No functions found.'));
15
+ return;
16
+ }
17
+
18
+ console.log('');
19
+ result.data.forEach((fn: CodeFunction) => {
20
+ const status = fn.active
21
+ ? chalk.green('● active')
22
+ : chalk.gray('○ inactive');
23
+ const type =
24
+ fn.type === 'hook'
25
+ ? chalk.yellow('[hook]')
26
+ : chalk.cyan('[function]');
27
+ console.log(` ${status} ${type} ${chalk.white(fn.slug)}`);
28
+ if (fn.type === 'hook') {
29
+ console.log(chalk.gray(` trigger: ${fn.trigger_event}`));
30
+ }
31
+ });
32
+ console.log('');
33
+ }
34
+
35
+ export async function functionsCreate(slug: string) {
36
+ const filename = `${slug}.js`;
37
+ if (existsSync(filename)) {
38
+ console.log(chalk.yellow(`${filename} already exists.`));
39
+ return;
40
+ }
41
+
42
+ const template = `/**
43
+ * Globio Edge Function: ${slug}
44
+ * Invoke: npx @globio/cli functions invoke ${slug} --input '{"key":"value"}'
45
+ */
46
+ async function handler(input, globio) {
47
+ // input: the payload from the caller
48
+ // globio: injected SDK — access all Globio services
49
+ // Example: const player = await globio.doc.get('players', input.userId);
50
+
51
+ return {
52
+ ok: true,
53
+ received: input,
54
+ };
55
+ }
56
+ `;
57
+ writeFileSync(filename, template);
58
+ console.log(chalk.green(`Created ${filename}`));
59
+ console.log(
60
+ chalk.gray(`Deploy with: npx @globio/cli functions deploy ${slug}`)
61
+ );
62
+ }
63
+
64
+ export async function functionsDeploy(
65
+ slug: string,
66
+ options: { file?: string; name?: string }
67
+ ) {
68
+ const filename = options.file ?? `${slug}.js`;
69
+ if (!existsSync(filename)) {
70
+ console.log(
71
+ chalk.red(
72
+ `File not found: ${filename}. Create it with: npx @globio/cli functions create ${slug}`
73
+ )
74
+ );
75
+ process.exit(1);
76
+ }
77
+
78
+ const code = readFileSync(filename, 'utf-8');
79
+ const client = getClient();
80
+ const spinner = ora(`Deploying ${slug}...`).start();
81
+ const existing = await client.code.getFunction(slug);
82
+
83
+ let result;
84
+ if (existing.success) {
85
+ result = await client.code.updateFunction(slug, {
86
+ code,
87
+ name: options.name ?? slug,
88
+ });
89
+ } else {
90
+ result = await client.code.createFunction({
91
+ name: options.name ?? slug,
92
+ slug,
93
+ type: 'function',
94
+ code,
95
+ });
96
+ }
97
+
98
+ if (!result.success) {
99
+ spinner.fail('Deploy failed');
100
+ console.error(result.error.message);
101
+ process.exit(1);
102
+ }
103
+
104
+ spinner.succeed(existing.success ? `Updated ${slug}` : `Deployed ${slug}`);
105
+ }
106
+
107
+ export async function functionsInvoke(
108
+ slug: string,
109
+ options: { input?: string }
110
+ ) {
111
+ let input: Record<string, unknown> = {};
112
+ if (options.input) {
113
+ try {
114
+ input = JSON.parse(options.input) as Record<string, unknown>;
115
+ } catch {
116
+ console.error(chalk.red('--input must be valid JSON'));
117
+ process.exit(1);
118
+ }
119
+ }
120
+
121
+ const client = getClient();
122
+ const spinner = ora(`Invoking ${slug}...`).start();
123
+ const result = await client.code.invoke(slug, input);
124
+ spinner.stop();
125
+
126
+ if (!result.success) {
127
+ console.log(chalk.red('Invocation failed'));
128
+ console.error(result.error.message);
129
+ return;
130
+ }
131
+
132
+ console.log('');
133
+ console.log(chalk.cyan('Result:'));
134
+ console.log(JSON.stringify(result.data.result, null, 2));
135
+ console.log(chalk.gray(`\nDuration: ${result.data.duration_ms}ms`));
136
+ }
137
+
138
+ export async function functionsLogs(
139
+ slug: string,
140
+ options: { limit?: string }
141
+ ) {
142
+ const limit = options.limit ? parseInt(options.limit, 10) : 20;
143
+ const client = getClient();
144
+ const spinner = ora('Fetching invocations...').start();
145
+ const result = await client.code.getInvocations(slug, limit);
146
+ spinner.stop();
147
+
148
+ if (!result.success || !result.data.length) {
149
+ console.log(chalk.gray('No invocations yet.'));
150
+ return;
151
+ }
152
+
153
+ console.log('');
154
+ result.data.forEach((inv: CodeInvocation) => {
155
+ const status = inv.success ? chalk.green('✓') : chalk.red('✗');
156
+ const date = new Date(inv.invoked_at * 1000)
157
+ .toISOString()
158
+ .replace('T', ' ')
159
+ .slice(0, 19);
160
+ console.log(
161
+ ` ${status} ${chalk.gray(date)} ${inv.duration_ms}ms ${chalk.gray(`[${inv.trigger_type}]`)}`
162
+ );
163
+ });
164
+ console.log('');
165
+ }
166
+
167
+ export async function functionsDelete(slug: string) {
168
+ const client = getClient();
169
+ const spinner = ora(`Deleting ${slug}...`).start();
170
+ const result = await client.code.deleteFunction(slug);
171
+ if (!result.success) {
172
+ spinner.fail(`Delete failed for ${slug}`);
173
+ console.error(result.error.message);
174
+ process.exit(1);
175
+ }
176
+ spinner.succeed(`Deleted ${slug}`);
177
+ }
178
+
179
+ export async function functionsToggle(slug: string, active: boolean) {
180
+ const client = getClient();
181
+ const spinner = ora(
182
+ `${active ? 'Enabling' : 'Disabling'} ${slug}...`
183
+ ).start();
184
+ const result = await client.code.toggleFunction(slug, active);
185
+ if (!result.success) {
186
+ spinner.fail(`Toggle failed for ${slug}`);
187
+ console.error(result.error.message);
188
+ process.exit(1);
189
+ }
190
+ spinner.succeed(`${slug} is now ${active ? 'active' : 'inactive'}`);
191
+ }
@@ -0,0 +1,65 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
4
+ import { config } from '../lib/config.js';
5
+ import { promptInit } from '../prompts/init.js';
6
+ import { migrateFirestore, migrateFirebaseStorage } from './migrate.js';
7
+
8
+ export async function init() {
9
+ console.log('');
10
+ p.intro(chalk.bgCyan(chalk.black(' Globio — Game Backend as a Service ')));
11
+
12
+ const values = await promptInit();
13
+
14
+ config.set({
15
+ apiKey: values.apiKey as string,
16
+ projectId: values.projectId as string,
17
+ });
18
+
19
+ if (!existsSync('globio.config.ts')) {
20
+ writeFileSync(
21
+ 'globio.config.ts',
22
+ `import { GlobioClient } from '@globio/sdk';
23
+
24
+ export const globio = new GlobioClient({
25
+ apiKey: process.env.GLOBIO_API_KEY!,
26
+ });
27
+ `
28
+ );
29
+ console.log(chalk.green('✓ Created globio.config.ts'));
30
+ }
31
+
32
+ if (!existsSync('.env')) {
33
+ writeFileSync('.env', `GLOBIO_API_KEY=${values.apiKey}\n`);
34
+ console.log(chalk.green('✓ Created .env'));
35
+ }
36
+
37
+ if (values.migrateFromFirebase && values.serviceAccountPath) {
38
+ console.log('');
39
+ console.log(chalk.cyan('Starting Firebase migration...'));
40
+
41
+ await migrateFirestore({
42
+ from: values.serviceAccountPath as string,
43
+ all: true,
44
+ });
45
+
46
+ const serviceAccount = JSON.parse(
47
+ readFileSync(values.serviceAccountPath as string, 'utf-8')
48
+ ) as { project_id: string };
49
+
50
+ await migrateFirebaseStorage({
51
+ from: values.serviceAccountPath as string,
52
+ bucket: `${serviceAccount.project_id}.appspot.com`,
53
+ all: true,
54
+ });
55
+ }
56
+
57
+ console.log('');
58
+ p.outro(
59
+ chalk.green('Your Globio project is ready.') +
60
+ '\n\n' +
61
+ chalk.white(' Next steps:\n') +
62
+ chalk.gray(' npm install @globio/sdk\n') +
63
+ chalk.gray(' npx @globio/cli functions create my-first-function\n')
64
+ );
65
+ }
@@ -0,0 +1,179 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { basename } from 'path';
4
+ import { initFirebase } from '../lib/firebase.js';
5
+ import { createProgressBar } from '../lib/progress.js';
6
+ import { getClient } from '../lib/sdk.js';
7
+
8
+ interface MigrateFirestoreOptions {
9
+ from: string;
10
+ collection?: string;
11
+ all?: boolean;
12
+ }
13
+
14
+ interface MigrateStorageOptions {
15
+ from: string;
16
+ bucket: string;
17
+ folder?: string;
18
+ all?: boolean;
19
+ }
20
+
21
+ export async function migrateFirestore(options: MigrateFirestoreOptions) {
22
+ console.log('');
23
+ p.intro(chalk.bgYellow(chalk.black(' Globio Migration — Firestore ')));
24
+
25
+ const { firestore } = await initFirebase(options.from);
26
+ const client = getClient();
27
+
28
+ let collections: string[] = [];
29
+
30
+ if (options.all) {
31
+ const snapshot = await firestore.listCollections();
32
+ collections = snapshot.map((collection) => collection.id);
33
+ console.log(
34
+ chalk.cyan(
35
+ `Found ${collections.length} collections: ${collections.join(', ')}`
36
+ )
37
+ );
38
+ } else if (options.collection) {
39
+ collections = [options.collection];
40
+ } else {
41
+ console.log(chalk.red('Specify --collection <name> or --all'));
42
+ process.exit(1);
43
+ }
44
+
45
+ const results: Record<
46
+ string,
47
+ { success: number; failed: number; failedIds: string[] }
48
+ > = {};
49
+
50
+ for (const collectionId of collections) {
51
+ console.log('');
52
+ console.log(chalk.cyan(`Migrating collection: ${collectionId}`));
53
+
54
+ const countSnap = await firestore.collection(collectionId).count().get();
55
+ const total = countSnap.data().count;
56
+
57
+ const bar = createProgressBar(collectionId);
58
+ bar.start(total, 0);
59
+
60
+ results[collectionId] = {
61
+ success: 0,
62
+ failed: 0,
63
+ failedIds: [],
64
+ };
65
+
66
+ let lastDoc: unknown = null;
67
+ let processed = 0;
68
+
69
+ while (processed < total) {
70
+ let query = firestore.collection(collectionId).limit(100);
71
+
72
+ if (lastDoc) {
73
+ query = query.startAfter(lastDoc as never);
74
+ }
75
+
76
+ const snapshot = await query.get();
77
+ if (snapshot.empty) {
78
+ break;
79
+ }
80
+
81
+ for (const doc of snapshot.docs) {
82
+ try {
83
+ const result = await client.doc.set(collectionId, doc.id, doc.data());
84
+ if (!result.success) {
85
+ throw new Error(result.error.message);
86
+ }
87
+ results[collectionId].success++;
88
+ } catch {
89
+ results[collectionId].failed++;
90
+ results[collectionId].failedIds.push(doc.id);
91
+ }
92
+ processed++;
93
+ bar.update(processed);
94
+ }
95
+
96
+ lastDoc = snapshot.docs[snapshot.docs.length - 1] ?? null;
97
+ }
98
+
99
+ bar.stop();
100
+
101
+ console.log(
102
+ chalk.green(` ✓ ${results[collectionId].success} documents migrated`)
103
+ );
104
+ if (results[collectionId].failed > 0) {
105
+ console.log(chalk.red(` ✗ ${results[collectionId].failed} failed`));
106
+ console.log(
107
+ chalk.gray(
108
+ ' Failed IDs: ' +
109
+ results[collectionId].failedIds.slice(0, 10).join(', ') +
110
+ (results[collectionId].failedIds.length > 10 ? '...' : '')
111
+ )
112
+ );
113
+ }
114
+ }
115
+
116
+ console.log('');
117
+ p.outro(chalk.green('Firestore migration complete.'));
118
+ console.log(
119
+ chalk.gray(
120
+ 'Your Firebase data is intact. Delete it manually when ready.'
121
+ )
122
+ );
123
+ }
124
+
125
+ export async function migrateFirebaseStorage(options: MigrateStorageOptions) {
126
+ console.log('');
127
+ p.intro(chalk.bgYellow(chalk.black(' Globio Migration — Storage ')));
128
+
129
+ const { storage } = await initFirebase(options.from);
130
+ const client = getClient();
131
+
132
+ const bucketName = options.bucket.replace(/^gs:\/\//, '');
133
+ const bucket = storage.bucket(bucketName);
134
+ const prefix = options.folder ? options.folder.replace(/^\//, '') : '';
135
+
136
+ const [files] = await bucket.getFiles(prefix ? { prefix } : {});
137
+
138
+ console.log(chalk.cyan(`Found ${files.length} files to migrate`));
139
+
140
+ const bar = createProgressBar('Storage');
141
+ bar.start(files.length, 0);
142
+
143
+ let success = 0;
144
+ let failed = 0;
145
+
146
+ for (const file of files) {
147
+ try {
148
+ const [buffer] = await file.download();
149
+ const uploadFile = new File(
150
+ [new Uint8Array(buffer)],
151
+ basename(file.name) || file.name
152
+ );
153
+ const result = await client.vault.uploadFile(uploadFile, {
154
+ metadata: {
155
+ original_path: file.name,
156
+ },
157
+ });
158
+
159
+ if (!result.success) {
160
+ throw new Error(result.error.message);
161
+ }
162
+
163
+ success++;
164
+ } catch {
165
+ failed++;
166
+ }
167
+ bar.increment();
168
+ }
169
+
170
+ bar.stop();
171
+
172
+ console.log('');
173
+ console.log(chalk.green(` ✓ ${success} files migrated`));
174
+ if (failed > 0) {
175
+ console.log(chalk.red(` ✗ ${failed} failed`));
176
+ }
177
+
178
+ p.outro(chalk.green('Storage migration complete.'));
179
+ }
@@ -0,0 +1,16 @@
1
+ import chalk from 'chalk';
2
+ import { config } from '../lib/config.js';
3
+
4
+ export async function projectsList() {
5
+ const cfg = config.get();
6
+ console.log('');
7
+ console.log(
8
+ chalk.cyan('Active project: ') + (cfg.projectId ?? chalk.gray('none'))
9
+ );
10
+ console.log('');
11
+ }
12
+
13
+ export async function projectsUse(projectId: string) {
14
+ config.set({ projectId });
15
+ console.log(chalk.green('Active project set to: ') + chalk.cyan(projectId));
16
+ }
@@ -0,0 +1,27 @@
1
+ import chalk from 'chalk';
2
+
3
+ const ALL_SERVICES = [
4
+ 'id',
5
+ 'doc',
6
+ 'vault',
7
+ 'pulse',
8
+ 'scope',
9
+ 'sync',
10
+ 'signal',
11
+ 'mart',
12
+ 'brain',
13
+ 'code',
14
+ ];
15
+
16
+ export async function servicesList() {
17
+ console.log('');
18
+ console.log(chalk.cyan('Available Globio services:'));
19
+ ALL_SERVICES.forEach((service) => {
20
+ console.log(' ' + chalk.white(service));
21
+ });
22
+ console.log('');
23
+ console.log(
24
+ chalk.gray('Manage service access via console.globio.stanlink.online')
25
+ );
26
+ console.log('');
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { login } from './auth/login.js';
4
+ import { logout } from './auth/logout.js';
5
+ import { whoami } from './auth/whoami.js';
6
+ import { init } from './commands/init.js';
7
+ import { projectsList, projectsUse } from './commands/projects.js';
8
+ import { servicesList } from './commands/services.js';
9
+ import {
10
+ functionsList,
11
+ functionsCreate,
12
+ functionsDeploy,
13
+ functionsInvoke,
14
+ functionsLogs,
15
+ functionsDelete,
16
+ functionsToggle,
17
+ } from './commands/functions.js';
18
+ import {
19
+ migrateFirestore,
20
+ migrateFirebaseStorage,
21
+ } from './commands/migrate.js';
22
+
23
+ const program = new Command();
24
+
25
+ program
26
+ .name('globio')
27
+ .description('The official Globio CLI')
28
+ .version('0.1.0');
29
+
30
+ program.command('login').description('Log in to your Globio account').action(login);
31
+ program.command('logout').description('Log out').action(logout);
32
+ program.command('whoami').description('Show current account and project').action(whoami);
33
+
34
+ program.command('init').description('Initialize a Globio project').action(init);
35
+
36
+ const projects = program.command('projects').description('Manage projects');
37
+ projects.command('list').description('List projects').action(projectsList);
38
+ projects.command('use <projectId>').description('Set active project').action(projectsUse);
39
+
40
+ program.command('services').description('List available Globio services').action(servicesList);
41
+
42
+ const functions = program
43
+ .command('functions')
44
+ .alias('fn')
45
+ .description('Manage GlobalCode edge functions');
46
+
47
+ functions.command('list').description('List all functions').action(functionsList);
48
+ functions.command('create <slug>').description('Scaffold a new function file locally').action(functionsCreate);
49
+ functions
50
+ .command('deploy <slug>')
51
+ .description('Deploy a function to GlobalCode')
52
+ .option('-f, --file <path>', 'Path to function file')
53
+ .option('-n, --name <name>', 'Display name')
54
+ .action(functionsDeploy);
55
+ functions
56
+ .command('invoke <slug>')
57
+ .description('Invoke a function')
58
+ .option('-i, --input <json>', 'JSON input payload')
59
+ .action(functionsInvoke);
60
+ functions
61
+ .command('logs <slug>')
62
+ .description('Show invocation history')
63
+ .option('-l, --limit <n>', 'Number of entries', '20')
64
+ .action(functionsLogs);
65
+ functions.command('delete <slug>').description('Delete a function').action(functionsDelete);
66
+ functions.command('enable <slug>').description('Enable a function').action((slug) => functionsToggle(slug, true));
67
+ functions.command('disable <slug>').description('Disable a function').action((slug) => functionsToggle(slug, false));
68
+
69
+ const migrate = program
70
+ .command('migrate')
71
+ .description('Migrate from Firebase to Globio');
72
+
73
+ migrate
74
+ .command('firestore')
75
+ .description('Migrate Firestore collections to GlobalDoc')
76
+ .requiredOption('--from <path>', 'Path to Firebase service account JSON')
77
+ .option('--collection <name>', 'Migrate a specific collection')
78
+ .option('--all', 'Migrate all collections')
79
+ .action(migrateFirestore);
80
+
81
+ migrate
82
+ .command('firebase-storage')
83
+ .description('Migrate Firebase Storage to GlobalVault')
84
+ .requiredOption('--from <path>', 'Path to Firebase service account JSON')
85
+ .requiredOption('--bucket <name>', 'Firebase Storage bucket')
86
+ .option('--folder <path>', 'Migrate a specific folder')
87
+ .option('--all', 'Migrate all files')
88
+ .action(migrateFirebaseStorage);
89
+
90
+ await program.parseAsync();