@globio/cli 0.1.9 → 0.2.2

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,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,
@@ -55,67 +56,103 @@ program
55
56
  .command('login')
56
57
  .description('Log in to your Globio account')
57
58
  .option('-p, --profile <name>', 'Profile name', 'default')
58
- .option('--token', 'Use a personal access token')
59
+ .option('--token <pat>', 'Personal access token (non-interactive)')
60
+ .option('--json', 'Output as JSON')
59
61
  .action(login);
60
62
  program.command('logout').description('Log out').option('--profile <name>', 'Use a specific profile').action(logout);
61
- program.command('whoami').description('Show current account and project').option('--profile <name>', 'Use a specific profile').action(whoami);
63
+ program.command('whoami').description('Show current account and project').option('--profile <name>', 'Use a specific profile').option('--json', 'Output as JSON').action(whoami);
62
64
  program.command('use <profile>').description('Switch active profile').action(useProfile);
63
65
 
64
- program.command('init').description('Initialize a Globio project').option('--profile <name>', 'Use a specific profile').action(init);
66
+ program
67
+ .command('init')
68
+ .description('Initialize a Globio project')
69
+ .option('-p, --profile <name>', 'Profile name')
70
+ .option('--name <name>', 'Project name')
71
+ .option('--slug <slug>', 'Project slug')
72
+ .option('--org <orgId>', 'Organization ID')
73
+ .option('--env <environment>', 'Environment (development|staging|production)', 'development')
74
+ .option('--no-migrate', 'Skip Firebase migration prompt')
75
+ .option('--from <path>', 'Firebase service account path (triggers migration)')
76
+ .option('--json', 'Output as JSON')
77
+ .action(init);
65
78
 
66
79
  const profiles = program
67
80
  .command('profiles')
68
81
  .description('Manage login profiles')
69
- .action(profilesList);
82
+ .option('--json', 'Output as JSON')
83
+ .action(function (this: Command) {
84
+ return profilesList(this.opts());
85
+ });
70
86
 
71
87
  profiles
72
88
  .command('list')
73
89
  .description('List all profiles')
74
- .action(profilesList);
90
+ .option('--json', 'Output as JSON')
91
+ .action(function (this: Command) {
92
+ return profilesList(this.opts());
93
+ });
75
94
 
76
95
  const projects = program.command('projects').description('Manage projects');
77
- projects.command('list').description('List projects').option('--profile <name>', 'Use a specific profile').action(projectsList);
78
- projects.command('create').description('Create a project').option('--profile <name>', 'Use a specific profile').action(projectsCreate);
79
- projects.command('use <projectId>').description('Set active project').option('--profile <name>', 'Use a specific profile').action(projectsUse);
80
-
81
- program.command('services').description('List available Globio services').option('--profile <name>', 'Use a specific profile').action(servicesList);
96
+ projects.command('list').description('List projects').option('--profile <name>', 'Use a specific profile').option('--json', 'Output as JSON').action(projectsList);
97
+ projects
98
+ .command('create')
99
+ .description('Create a project')
100
+ .option('--name <name>', 'Project name')
101
+ .option('--org <orgId>', 'Organization ID')
102
+ .option('--env <environment>', 'Environment', 'development')
103
+ .option('-p, --profile <name>', 'Profile name')
104
+ .option('--json', 'Output as JSON')
105
+ .action(projectsCreate);
106
+ projects.command('use <projectId>').description('Set active project').option('--profile <name>', 'Use a specific profile').option('--json', 'Output as JSON').action(projectsUse);
107
+
108
+ program.command('services').description('List available Globio services').option('--profile <name>', 'Use a specific profile').option('--json', 'Output as JSON').action(servicesList);
82
109
 
83
110
  const functions = program
84
111
  .command('functions')
85
112
  .alias('fn')
86
113
  .description('Manage GlobalCode edge functions');
87
114
 
88
- functions.command('list').description('List all functions').option('--profile <name>', 'Use a specific profile').action(functionsList);
89
- functions.command('create <slug>').description('Scaffold a new function file locally').option('--profile <name>', 'Use a specific profile').action(functionsCreate);
115
+ functions.command('list').description('List all functions').option('--profile <name>', 'Use a specific profile').option('--json', 'Output as JSON').action(functionsList);
116
+ functions.command('create <slug>').description('Scaffold a new function file locally').option('--profile <name>', 'Use a specific profile').option('--json', 'Output as JSON').action(functionsCreate);
90
117
  functions
91
118
  .command('deploy <slug>')
92
119
  .description('Deploy a function to GlobalCode')
93
120
  .option('-f, --file <path>', 'Path to function file')
94
121
  .option('-n, --name <name>', 'Display name')
95
122
  .option('--profile <name>', 'Use a specific profile')
123
+ .option('--json', 'Output as JSON')
96
124
  .action(functionsDeploy);
97
125
  functions
98
126
  .command('invoke <slug>')
99
127
  .description('Invoke a function')
100
128
  .option('-i, --input <json>', 'JSON input payload')
101
129
  .option('--profile <name>', 'Use a specific profile')
130
+ .option('--json', 'Output as JSON')
102
131
  .action(functionsInvoke);
103
132
  functions
104
133
  .command('logs <slug>')
105
134
  .description('Show invocation history')
106
135
  .option('-l, --limit <n>', 'Number of entries', '20')
107
136
  .option('--profile <name>', 'Use a specific profile')
137
+ .option('--json', 'Output as JSON')
108
138
  .action(functionsLogs);
109
- functions.command('delete <slug>').description('Delete a function').option('--profile <name>', 'Use a specific profile').action(functionsDelete);
139
+ functions
140
+ .command('watch <slug>')
141
+ .description('Stream live function execution logs')
142
+ .option('--profile <name>', 'Use a specific profile')
143
+ .action(functionsWatch);
144
+ functions.command('delete <slug>').description('Delete a function').option('--profile <name>', 'Use a specific profile').option('--json', 'Output as JSON').action(functionsDelete);
110
145
  functions
111
146
  .command('enable <slug>')
112
147
  .description('Enable a function')
113
148
  .option('--profile <name>', 'Use a specific profile')
149
+ .option('--json', 'Output as JSON')
114
150
  .action((slug, options) => functionsToggle(slug, true, options));
115
151
  functions
116
152
  .command('disable <slug>')
117
153
  .description('Disable a function')
118
154
  .option('--profile <name>', 'Use a specific profile')
155
+ .option('--json', 'Output as JSON')
119
156
  .action((slug, options) => functionsToggle(slug, false, options));
120
157
 
121
158
  const migrate = program
@@ -129,6 +166,7 @@ migrate
129
166
  .option('--collection <name>', 'Migrate a specific collection')
130
167
  .option('--all', 'Migrate all collections')
131
168
  .option('--profile <name>', 'Use a specific profile')
169
+ .option('--json', 'Output as JSON')
132
170
  .action(migrateFirestore);
133
171
 
134
172
  migrate
@@ -139,6 +177,7 @@ migrate
139
177
  .option('--folder <path>', 'Migrate a specific folder')
140
178
  .option('--all', 'Migrate all files')
141
179
  .option('--profile <name>', 'Use a specific profile')
180
+ .option('--json', 'Output as JSON')
142
181
  .action(migrateFirebaseStorage);
143
182
 
144
183
  async function main() {
package/src/lib/table.ts CHANGED
@@ -95,3 +95,8 @@ export function header(version: string, subtitle?: string): string {
95
95
  export function footer(text: string): string {
96
96
  return '\n' + dim(' ' + text) + '\n';
97
97
  }
98
+
99
+ export function jsonOutput(data: unknown): void {
100
+ console.log(JSON.stringify(data, null, 2));
101
+ process.exit(0);
102
+ }