@globio/cli 0.1.8 → 0.2.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.
@@ -1,30 +1,70 @@
1
- import chalk from 'chalk';
2
1
  import { config } from '../lib/config.js';
2
+ import { manageRequest, type ManageProjectServices } from '../lib/manage.js';
3
+ import {
4
+ footer,
5
+ getCliVersion,
6
+ green,
7
+ header,
8
+ inactive,
9
+ muted,
10
+ orange,
11
+ renderTable,
12
+ } from '../lib/banner.js';
3
13
 
4
- const ALL_SERVICES = [
5
- 'id',
6
- 'doc',
7
- 'vault',
8
- 'pulse',
9
- 'scope',
10
- 'sync',
11
- 'signal',
12
- 'mart',
13
- 'brain',
14
- 'code',
15
- ];
14
+ const version = getCliVersion();
15
+
16
+ const SERVICE_DESCRIPTIONS: Record<string, string> = {
17
+ id: 'Authentication and user management',
18
+ doc: 'Document database',
19
+ vault: 'File storage',
20
+ pulse: 'Feature flags and remote config',
21
+ scope: 'Analytics and event tracking',
22
+ sync: 'Real-time multiplayer rooms',
23
+ signal: 'Push notifications',
24
+ mart: 'Game economy and payments',
25
+ brain: 'AI agents and LLM routing',
26
+ code: 'Edge functions and GC Hooks',
27
+ };
16
28
 
17
29
  export async function servicesList(options: { profile?: string } = {}) {
18
- void options.profile;
19
- void config;
20
- console.log('');
21
- console.log(chalk.cyan('Available Globio services:'));
22
- ALL_SERVICES.forEach((service) => {
23
- console.log(' ' + chalk.white(service));
30
+ const profileName = options.profile ?? config.getActiveProfile() ?? 'default';
31
+ const profile = config.getProfile(profileName);
32
+ let serviceStatuses: ManageProjectServices = {};
33
+
34
+ if (profile?.active_project_id) {
35
+ try {
36
+ serviceStatuses = await manageRequest<ManageProjectServices>(
37
+ `/projects/${profile.active_project_id}/services`,
38
+ { profileName }
39
+ );
40
+ } catch {
41
+ serviceStatuses = {};
42
+ }
43
+ }
44
+
45
+ const rows = Object.entries(SERVICE_DESCRIPTIONS).map(([slug, desc]) => {
46
+ const enabled = serviceStatuses[slug] ?? null;
47
+ return [
48
+ orange(slug),
49
+ muted(desc),
50
+ enabled === true
51
+ ? green('enabled')
52
+ : enabled === false
53
+ ? inactive('disabled')
54
+ : inactive('—'),
55
+ ];
24
56
  });
25
- console.log('');
57
+
58
+ console.log(header(version));
26
59
  console.log(
27
- chalk.gray('Manage service access via console.globio.stanlink.online')
60
+ renderTable({
61
+ columns: [
62
+ { header: 'Service', width: 10 },
63
+ { header: 'Description', width: 42 },
64
+ { header: 'Status', width: 10 },
65
+ ],
66
+ rows,
67
+ })
28
68
  );
29
- console.log('');
69
+ console.log(footer('Manage services at console.globio.stanlink.online'));
30
70
  }
@@ -0,0 +1,173 @@
1
+ import { config } from '../lib/config.js';
2
+ import {
3
+ dim,
4
+ failure,
5
+ getCliVersion,
6
+ green,
7
+ header,
8
+ muted,
9
+ orange,
10
+ reset,
11
+ } from '../lib/banner.js';
12
+
13
+ const BASE_URL = 'https://api.globio.stanlink.online';
14
+ const version = getCliVersion();
15
+
16
+ export async function functionsWatch(
17
+ slug: string,
18
+ options: { profile?: string } = {}
19
+ ) {
20
+ const profileName = options.profile ?? config.getActiveProfile();
21
+ const profile = config.getProfile(profileName ?? 'default');
22
+
23
+ if (!profile?.project_api_key) {
24
+ console.log(
25
+ failure('No active project.') +
26
+ reset +
27
+ ' Run: globio projects use <id>'
28
+ );
29
+ process.exit(1);
30
+ }
31
+
32
+ console.log(header(version));
33
+ console.log(
34
+ ' ' +
35
+ orange('watching') +
36
+ reset +
37
+ ' ' +
38
+ slug +
39
+ dim(' · press Ctrl+C to stop') +
40
+ '\n'
41
+ );
42
+
43
+ const res = await fetch(`${BASE_URL}/code/functions/${slug}/watch`, {
44
+ headers: {
45
+ 'X-Globio-Key': profile.project_api_key,
46
+ Accept: 'text/event-stream',
47
+ },
48
+ });
49
+
50
+ if (!res.ok || !res.body) {
51
+ console.log(failure('Failed to connect to watch stream.') + reset);
52
+ process.exit(1);
53
+ }
54
+
55
+ const reader = res.body.getReader();
56
+ const decoder = new TextDecoder();
57
+ let buffer = '';
58
+
59
+ process.on('SIGINT', () => {
60
+ console.log('\n' + dim(' Stream closed.') + '\n');
61
+ void reader.cancel();
62
+ process.exit(0);
63
+ });
64
+
65
+ while (true) {
66
+ const { done, value } = await reader.read();
67
+ if (done) break;
68
+
69
+ buffer += decoder.decode(value, { stream: true });
70
+ const chunks = buffer.split('\n\n');
71
+ buffer = chunks.pop() ?? '';
72
+
73
+ for (const chunk of chunks) {
74
+ const dataLine = chunk
75
+ .split('\n')
76
+ .find((line) => line.startsWith('data: '));
77
+
78
+ if (!dataLine) continue;
79
+
80
+ try {
81
+ renderEvent(JSON.parse(dataLine.slice(6)) as WatchEvent);
82
+ } catch {
83
+ // Ignore malformed events.
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ interface WatchEvent {
90
+ type: 'connected' | 'heartbeat' | 'timeout' | 'invocation';
91
+ invoked_at?: number;
92
+ trigger_type?: string;
93
+ duration_ms?: number;
94
+ success?: boolean;
95
+ input?: string | null;
96
+ result?: string | null;
97
+ error_message?: string | null;
98
+ logs?: string | null;
99
+ }
100
+
101
+ function renderEvent(event: WatchEvent) {
102
+ if (event.type === 'connected') {
103
+ console.log(
104
+ ' ' + green('●') + reset + dim(' connected — waiting for invocations...\n')
105
+ );
106
+ return;
107
+ }
108
+
109
+ if (event.type === 'heartbeat') {
110
+ return;
111
+ }
112
+
113
+ if (event.type === 'timeout') {
114
+ console.log(
115
+ '\n' + dim(' Session timed out after 5 minutes.') + ' Run again to resume.\n'
116
+ );
117
+ return;
118
+ }
119
+
120
+ if (event.type !== 'invocation' || !event.invoked_at) {
121
+ return;
122
+ }
123
+
124
+ const time = new Date(event.invoked_at * 1000)
125
+ .toISOString()
126
+ .replace('T', ' ')
127
+ .slice(0, 19);
128
+
129
+ const status = event.success ? green('✓') : failure('✗');
130
+ const trigger = dim(`[${event.trigger_type ?? 'http'}]`);
131
+ const duration = dim(`${event.duration_ms ?? 0}ms`);
132
+
133
+ console.log(
134
+ ' ' + status + reset + ' ' + dim(time) + ' ' + trigger + ' ' + duration
135
+ );
136
+
137
+ if (event.input && event.input !== '{}') {
138
+ try {
139
+ console.log(
140
+ ' ' + dim(' input ') + muted(JSON.stringify(JSON.parse(event.input)))
141
+ );
142
+ } catch {
143
+ // Ignore invalid JSON payloads.
144
+ }
145
+ }
146
+
147
+ if (event.logs) {
148
+ try {
149
+ const logs = JSON.parse(event.logs) as string[];
150
+ for (const line of logs) {
151
+ console.log(' ' + dim(' log ') + reset + line);
152
+ }
153
+ } catch {
154
+ // Ignore invalid log payloads.
155
+ }
156
+ }
157
+
158
+ if (event.result && event.result !== 'null') {
159
+ try {
160
+ console.log(
161
+ ' ' + dim(' result ') + muted(JSON.stringify(JSON.parse(event.result)))
162
+ );
163
+ } catch {
164
+ // Ignore invalid result payloads.
165
+ }
166
+ }
167
+
168
+ if (event.error_message) {
169
+ console.log(' ' + dim(' error ') + failure(event.error_message) + reset);
170
+ }
171
+
172
+ console.log('');
173
+ }
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  functionsDelete,
18
18
  functionsToggle,
19
19
  } from './commands/functions.js';
20
+ import { functionsWatch } from './commands/watch.js';
20
21
  import {
21
22
  migrateFirestore,
22
23
  migrateFirebaseStorage,
@@ -106,6 +107,11 @@ functions
106
107
  .option('-l, --limit <n>', 'Number of entries', '20')
107
108
  .option('--profile <name>', 'Use a specific profile')
108
109
  .action(functionsLogs);
110
+ functions
111
+ .command('watch <slug>')
112
+ .description('Stream live function execution logs')
113
+ .option('--profile <name>', 'Use a specific profile')
114
+ .action(functionsWatch);
109
115
  functions.command('delete <slug>').description('Delete a function').option('--profile <name>', 'Use a specific profile').action(functionsDelete);
110
116
  functions
111
117
  .command('enable <slug>')
package/src/lib/banner.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import figlet from 'figlet';
3
3
  import gradientString from 'gradient-string';
4
+ export * from './table.js';
4
5
 
5
6
  const globioGradient = gradientString(
6
7
  '#e85d04',
@@ -39,12 +40,6 @@ export function printInfo(message: string) {
39
40
  console.log('\x1b[2m›\x1b[0m ' + message);
40
41
  }
41
42
 
42
- export const orange = (s: string) => '\x1b[38;2;244;140;6m' + s + '\x1b[0m';
43
-
44
- export const gold = (s: string) => '\x1b[38;2;255;208;0m' + s + '\x1b[0m';
45
-
46
- export const muted = (s: string) => '\x1b[2m' + s + '\x1b[0m';
47
-
48
43
  export function getCliVersion() {
49
44
  const file = readFileSync(new URL('../package.json', import.meta.url), 'utf8');
50
45
  return (JSON.parse(file) as { version: string }).version;
package/src/lib/config.ts CHANGED
@@ -11,6 +11,7 @@ export interface ProfileData {
11
11
  pat: string;
12
12
  account_email: string;
13
13
  account_name: string;
14
+ org_name?: string;
14
15
  active_project_id?: string;
15
16
  active_project_name?: string;
16
17
  project_api_key?: string;
@@ -88,6 +89,7 @@ export const config = {
88
89
  pat: data.pat ?? existing?.pat ?? '',
89
90
  account_email: data.account_email ?? existing?.account_email ?? '',
90
91
  account_name: data.account_name ?? existing?.account_name ?? '',
92
+ org_name: data.org_name ?? existing?.org_name,
91
93
  active_project_id: data.active_project_id ?? existing?.active_project_id,
92
94
  active_project_name: data.active_project_name ?? existing?.active_project_name,
93
95
  project_api_key: data.project_api_key ?? existing?.project_api_key,
package/src/lib/manage.ts CHANGED
@@ -36,6 +36,10 @@ export interface ManageProject {
36
36
  active: boolean;
37
37
  }
38
38
 
39
+ export interface ManageProjectServices {
40
+ [key: string]: boolean;
41
+ }
42
+
39
43
  export interface ManageProjectKey {
40
44
  id: string;
41
45
  name: string;
@@ -0,0 +1,97 @@
1
+ export interface Column {
2
+ header: string;
3
+ width: number;
4
+ color?: (val: string) => string;
5
+ }
6
+
7
+ export interface TableOptions {
8
+ columns: Column[];
9
+ rows: string[][];
10
+ }
11
+
12
+ const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
13
+
14
+ export const orange = (s: string) => '\x1b[38;2;244;140;6m' + s;
15
+ export const gold = (s: string) => '\x1b[38;2;255;208;0m' + s;
16
+ export const dim = (s: string) => '\x1b[2m' + s + '\x1b[0m';
17
+ export const white = (s: string) => '\x1b[97m' + s;
18
+ export const green = (s: string) => '\x1b[38;2;34;197;94m' + s;
19
+ export const muted = (s: string) => '\x1b[38;2;85;85;85m' + s;
20
+ export const inactive = (s: string) => '\x1b[38;2;68;68;68m' + s;
21
+ export const failure = (s: string) => '\x1b[38;2;232;93;4m' + s;
22
+ export const reset = '\x1b[0m';
23
+
24
+ function stripAnsi(value: string): string {
25
+ return value.replace(ANSI_PATTERN, '');
26
+ }
27
+
28
+ function fitCell(value: string, width: number): string {
29
+ const plain = stripAnsi(value);
30
+ if (plain.length <= width) {
31
+ return value + ' '.repeat(width - plain.length);
32
+ }
33
+
34
+ const truncated = plain.slice(0, width);
35
+ return truncated;
36
+ }
37
+
38
+ export function renderTable(options: TableOptions): string {
39
+ const { columns, rows } = options;
40
+ const lines: string[] = [];
41
+
42
+ lines.push(
43
+ ' ┌' + columns.map((c) => '─'.repeat(c.width + 2)).join('┬') + '┐'
44
+ );
45
+
46
+ lines.push(
47
+ ' │' +
48
+ columns
49
+ .map((c) => ' ' + dim(c.header.padEnd(c.width)) + ' │')
50
+ .join('')
51
+ );
52
+
53
+ lines.push(
54
+ ' ├' + columns.map((c) => '─'.repeat(c.width + 2)).join('┼') + '┤'
55
+ );
56
+
57
+ for (const row of rows) {
58
+ lines.push(
59
+ ' │' +
60
+ columns
61
+ .map((c, i) => {
62
+ const raw = row[i] ?? '';
63
+ const fitted = fitCell(raw, c.width);
64
+ const colored = c.color
65
+ ? fitCell(c.color(stripAnsi(raw)), c.width)
66
+ : fitted;
67
+ return ' ' + colored + ' ' + reset + '│';
68
+ })
69
+ .join('')
70
+ );
71
+ }
72
+
73
+ lines.push(
74
+ ' └' + columns.map((c) => '─'.repeat(c.width + 2)).join('┴') + '┘'
75
+ );
76
+
77
+ return lines.join('\n');
78
+ }
79
+
80
+ export function header(version: string, subtitle?: string): string {
81
+ const lines = [
82
+ '',
83
+ orange(' ⇒⇒') + reset + ' globio ' + dim(version),
84
+ dim(' ──────────────────────────────────────────'),
85
+ ];
86
+
87
+ if (subtitle) {
88
+ lines.push(' ' + subtitle);
89
+ }
90
+
91
+ lines.push('');
92
+ return lines.join('\n');
93
+ }
94
+
95
+ export function footer(text: string): string {
96
+ return '\n' + dim(' ' + text) + '\n';
97
+ }