@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.
- package/dist/index.js +568 -139
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/auth/login.ts +86 -27
- package/src/auth/whoami.ts +27 -2
- package/src/commands/functions.ts +111 -20
- 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/commands/watch.ts +173 -0
- package/src/index.ts +52 -13
- package/src/lib/table.ts +5 -0
|
@@ -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', '
|
|
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
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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
|
+
}
|