@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.
- package/dist/index.js +384 -146
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/auth/login.ts +9 -10
- package/src/auth/logout.ts +5 -5
- package/src/auth/whoami.ts +58 -19
- package/src/commands/functions.ts +60 -34
- package/src/commands/init.ts +3 -2
- package/src/commands/migrate.ts +11 -10
- package/src/commands/profiles.ts +39 -12
- package/src/commands/projects.ts +51 -29
- package/src/commands/services.ts +62 -22
- package/src/commands/watch.ts +173 -0
- package/src/index.ts +6 -0
- package/src/lib/banner.ts +1 -6
- package/src/lib/config.ts +2 -0
- package/src/lib/manage.ts +4 -0
- package/src/lib/table.ts +97 -0
package/src/commands/services.ts
CHANGED
|
@@ -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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
57
|
+
|
|
58
|
+
console.log(header(version));
|
|
26
59
|
console.log(
|
|
27
|
-
|
|
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
package/src/lib/table.ts
ADDED
|
@@ -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
|
+
}
|