@globio/cli 0.2.0 → 0.2.3

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.
@@ -5,6 +5,7 @@ import {
5
5
  getCliVersion,
6
6
  gold,
7
7
  green,
8
+ jsonOutput,
8
9
  muted,
9
10
  orange,
10
11
  printBanner,
@@ -21,6 +22,7 @@ interface MigrateFirestoreOptions {
21
22
  collection?: string;
22
23
  all?: boolean;
23
24
  profile?: string;
25
+ json?: boolean;
24
26
  }
25
27
 
26
28
  interface MigrateStorageOptions {
@@ -29,15 +31,36 @@ interface MigrateStorageOptions {
29
31
  folder?: string;
30
32
  all?: boolean;
31
33
  profile?: string;
34
+ json?: boolean;
35
+ }
36
+
37
+ interface FirestoreCollectionResult {
38
+ name: string;
39
+ migrated: number;
40
+ failed: number;
41
+ failed_ids: string[];
42
+ }
43
+
44
+ interface FirestoreMigrationSummary {
45
+ collections: FirestoreCollectionResult[];
46
+ total_migrated: number;
47
+ total_failed: number;
48
+ }
49
+
50
+ interface StorageMigrationSummary {
51
+ migrated: number;
52
+ failed: number;
32
53
  }
33
54
 
34
55
  function resolveProfileName(profile?: string) {
35
56
  return profile ?? config.getActiveProfile() ?? 'default';
36
57
  }
37
58
 
38
- export async function migrateFirestore(options: MigrateFirestoreOptions) {
39
- printBanner(version);
40
- p.intro(gold('⇒⇒') + ' Firebase → Globio Migration');
59
+ export async function migrateFirestore(options: MigrateFirestoreOptions): Promise<void> {
60
+ if (!options.json) {
61
+ printBanner(version);
62
+ p.intro(gold('⇒⇒') + ' Firebase → Globio Migration');
63
+ }
41
64
 
42
65
  const { firestore } = await initFirebase(options.from);
43
66
  const profileName = resolveProfileName(options.profile);
@@ -47,32 +70,34 @@ export async function migrateFirestore(options: MigrateFirestoreOptions) {
47
70
  if (options.all) {
48
71
  const snapshot = await firestore.listCollections();
49
72
  collections = snapshot.map((collection) => collection.id);
50
- console.log(
51
- green(
52
- `Found ${collections.length} collections: ${collections.join(', ')}`
53
- )
54
- );
73
+ if (!options.json) {
74
+ console.log(
75
+ green(`Found ${collections.length} collections: ${collections.join(', ')}`)
76
+ );
77
+ }
55
78
  } else if (options.collection) {
56
79
  collections = [options.collection];
57
80
  } else {
81
+ if (options.json) {
82
+ jsonOutput({ success: false, error: 'Specify --collection <name> or --all' });
83
+ }
58
84
  console.log(failure('Specify --collection <name> or --all'));
59
85
  process.exit(1);
60
86
  }
61
87
 
62
- const results: Record<
63
- string,
64
- { success: number; failed: number; failedIds: string[] }
65
- > = {};
88
+ const results: Record<string, { success: number; failed: number; failedIds: string[] }> = {};
66
89
 
67
90
  for (const collectionId of collections) {
68
- console.log('');
69
- console.log(' ' + orange(collectionId));
91
+ if (!options.json) {
92
+ console.log('');
93
+ console.log(' ' + orange(collectionId));
94
+ }
70
95
 
71
96
  const countSnap = await firestore.collection(collectionId).count().get();
72
97
  const total = countSnap.data().count;
73
98
 
74
- const bar = createProgressBar(collectionId);
75
- bar.start(total, 0);
99
+ const bar = options.json ? null : createProgressBar(collectionId);
100
+ bar?.start(total, 0);
76
101
 
77
102
  results[collectionId] = {
78
103
  success: 0,
@@ -87,7 +112,6 @@ export async function migrateFirestore(options: MigrateFirestoreOptions) {
87
112
 
88
113
  while (processed < total) {
89
114
  let query = firestore.collection(collectionId).limit(100);
90
-
91
115
  if (lastDoc) {
92
116
  query = query.startAfter(lastDoc as never);
93
117
  }
@@ -119,35 +143,49 @@ export async function migrateFirestore(options: MigrateFirestoreOptions) {
119
143
  results[collectionId].failed++;
120
144
  results[collectionId].failedIds.push(doc.id);
121
145
  }
146
+
122
147
  processed++;
123
- bar.update(processed);
148
+ bar?.update(processed);
124
149
  }
125
150
 
126
151
  lastDoc = snapshot.docs[snapshot.docs.length - 1] ?? null;
127
152
  }
128
153
 
129
- bar.stop();
154
+ bar?.stop();
130
155
 
131
- console.log(
132
- green(` ✓ ${results[collectionId].success} documents migrated`)
133
- );
134
- if (indexFieldCount > 0) {
135
- console.log(
136
- muted(` Indexes created for ${indexFieldCount} fields`)
137
- );
138
- }
139
- if (results[collectionId].failed > 0) {
140
- console.log(failure(` ${results[collectionId].failed} failed`) + '\x1b[0m');
141
- console.log(
142
- muted(
143
- ' Failed IDs: ' +
144
- results[collectionId].failedIds.slice(0, 10).join(', ') +
145
- (results[collectionId].failedIds.length > 10 ? '...' : '')
146
- )
147
- );
156
+ if (!options.json) {
157
+ console.log(green(` ✓ ${results[collectionId].success} documents migrated`));
158
+ if (indexFieldCount > 0) {
159
+ console.log(muted(` Indexes created for ${indexFieldCount} fields`));
160
+ }
161
+ if (results[collectionId].failed > 0) {
162
+ console.log(failure(` ✗ ${results[collectionId].failed} failed`) + '\x1b[0m');
163
+ console.log(
164
+ muted(
165
+ ' Failed IDs: ' +
166
+ results[collectionId].failedIds.slice(0, 10).join(', ') +
167
+ (results[collectionId].failedIds.length > 10 ? '...' : '')
168
+ )
169
+ );
170
+ }
148
171
  }
149
172
  }
150
173
 
174
+ const summary: FirestoreMigrationSummary = {
175
+ collections: collections.map((name) => ({
176
+ name,
177
+ migrated: results[name]?.success ?? 0,
178
+ failed: results[name]?.failed ?? 0,
179
+ failed_ids: results[name]?.failedIds ?? [],
180
+ })),
181
+ total_migrated: collections.reduce((sum, name) => sum + (results[name]?.success ?? 0), 0),
182
+ total_failed: collections.reduce((sum, name) => sum + (results[name]?.failed ?? 0), 0),
183
+ };
184
+
185
+ if (options.json) {
186
+ jsonOutput(summary);
187
+ }
188
+
151
189
  console.log('');
152
190
  p.outro(
153
191
  orange('✓') +
@@ -158,11 +196,14 @@ export async function migrateFirestore(options: MigrateFirestoreOptions) {
158
196
  ' ' +
159
197
  muted('Delete it manually when ready.')
160
198
  );
199
+
161
200
  }
162
201
 
163
- export async function migrateFirebaseStorage(options: MigrateStorageOptions) {
164
- printBanner(version);
165
- p.intro(gold('⇒⇒') + ' Firebase → Globio Migration');
202
+ export async function migrateFirebaseStorage(options: MigrateStorageOptions): Promise<void> {
203
+ if (!options.json) {
204
+ printBanner(version);
205
+ p.intro(gold('⇒⇒') + ' Firebase → Globio Migration');
206
+ }
166
207
 
167
208
  const { storage } = await initFirebase(options.from);
168
209
  const profileName = resolveProfileName(options.profile);
@@ -178,10 +219,12 @@ export async function migrateFirebaseStorage(options: MigrateStorageOptions) {
178
219
 
179
220
  const [files] = await bucket.getFiles(prefix ? { prefix } : {});
180
221
 
181
- console.log(green(`Found ${files.length} files to migrate`));
222
+ if (!options.json) {
223
+ console.log(green(`Found ${files.length} files to migrate`));
224
+ }
182
225
 
183
- const bar = createProgressBar('Storage');
184
- bar.start(files.length, 0);
226
+ const bar = options.json ? null : createProgressBar('Storage');
227
+ bar?.start(files.length, 0);
185
228
 
186
229
  let success = 0;
187
230
  let failed = 0;
@@ -198,16 +241,13 @@ export async function migrateFirebaseStorage(options: MigrateStorageOptions) {
198
241
  );
199
242
  formData.append('path', file.name);
200
243
 
201
- const res = await fetch(
202
- 'https://api.globio.stanlink.online/vault/files',
203
- {
204
- method: 'POST',
205
- headers: {
206
- 'X-Globio-Key': profile.project_api_key,
207
- },
208
- body: formData,
209
- }
210
- );
244
+ const res = await fetch('https://api.globio.stanlink.online/vault/files', {
245
+ method: 'POST',
246
+ headers: {
247
+ 'X-Globio-Key': profile.project_api_key,
248
+ },
249
+ body: formData,
250
+ });
211
251
 
212
252
  if (!res.ok) {
213
253
  throw new Error(`Upload failed: ${res.status}`);
@@ -217,10 +257,20 @@ export async function migrateFirebaseStorage(options: MigrateStorageOptions) {
217
257
  } catch {
218
258
  failed++;
219
259
  }
220
- bar.increment();
260
+
261
+ bar?.increment();
221
262
  }
222
263
 
223
- bar.stop();
264
+ bar?.stop();
265
+
266
+ const summary: StorageMigrationSummary = {
267
+ migrated: success,
268
+ failed,
269
+ };
270
+
271
+ if (options.json) {
272
+ jsonOutput(summary);
273
+ }
224
274
 
225
275
  console.log('');
226
276
  console.log(green(` ✓ ${success} files migrated`));
@@ -237,4 +287,5 @@ export async function migrateFirebaseStorage(options: MigrateStorageOptions) {
237
287
  ' ' +
238
288
  muted('Delete it manually when ready.')
239
289
  );
290
+
240
291
  }
@@ -3,6 +3,7 @@ import {
3
3
  green,
4
4
  header,
5
5
  inactive,
6
+ jsonOutput,
6
7
  muted,
7
8
  orange,
8
9
  renderTable,
@@ -13,9 +14,23 @@ import { config } from '../lib/config.js';
13
14
 
14
15
  const version = getCliVersion();
15
16
 
16
- export async function profilesList() {
17
+ export async function profilesList(options: { json?: boolean } = {}) {
17
18
  const profiles = config.listProfiles();
18
19
  const active = config.getActiveProfile();
20
+ const wantsJson = options.json ?? process.argv.includes('--json');
21
+
22
+ if (wantsJson) {
23
+ jsonOutput(
24
+ profiles.map((name) => {
25
+ const profile = config.getProfile(name);
26
+ return {
27
+ name,
28
+ email: profile?.account_email ?? null,
29
+ active: name === active,
30
+ };
31
+ })
32
+ );
33
+ }
19
34
 
20
35
  if (!profiles.length) {
21
36
  console.log(
@@ -14,6 +14,7 @@ import {
14
14
  green,
15
15
  header,
16
16
  inactive,
17
+ jsonOutput,
17
18
  muted,
18
19
  orange,
19
20
  renderTable,
@@ -54,13 +55,65 @@ async function createServerKey(projectId: string, profileName: string) {
54
55
  return created.token;
55
56
  }
56
57
 
57
- export async function projectsList(options: { profile?: string } = {}) {
58
+ function buildSlug(value: string) {
59
+ return slugify(value).slice(0, 30);
60
+ }
61
+
62
+ async function createProjectRecord(
63
+ input: {
64
+ profileName: string;
65
+ orgId: string;
66
+ orgName?: string;
67
+ name: string;
68
+ slug?: string;
69
+ environment?: string;
70
+ }
71
+ ) {
72
+ const result = await manageRequest<{
73
+ project: { id: string; name: string; slug: string; environment: string; active: boolean };
74
+ keys: { client: string; server: string };
75
+ }>('/projects', {
76
+ method: 'POST',
77
+ body: {
78
+ org_id: input.orgId,
79
+ name: input.name,
80
+ slug: input.slug ?? buildSlug(input.name),
81
+ environment: input.environment ?? 'development',
82
+ },
83
+ profileName: input.profileName,
84
+ });
85
+
86
+ config.setProfile(input.profileName, {
87
+ active_project_id: result.project.id,
88
+ active_project_name: result.project.name,
89
+ org_name: input.orgName,
90
+ project_api_key: result.keys.server,
91
+ });
92
+ config.setActiveProfile(input.profileName);
93
+
94
+ return result;
95
+ }
96
+
97
+ export async function projectsList(options: { profile?: string; json?: boolean } = {}) {
58
98
  const profileName = resolveProfileName(options.profile);
59
99
  config.requireAuth(profileName);
60
100
 
61
101
  const projects = await manageRequest<ManageProject[]>('/projects', { profileName });
62
102
  const activeProjectId = config.getProfile(profileName)?.active_project_id;
63
103
 
104
+ if (options.json) {
105
+ jsonOutput(
106
+ projects.map((project) => ({
107
+ id: project.id,
108
+ name: project.name,
109
+ org_id: project.org_id,
110
+ org_name: project.org_name,
111
+ environment: project.environment,
112
+ active: project.id === activeProjectId,
113
+ }))
114
+ );
115
+ }
116
+
64
117
  if (!projects.length) {
65
118
  console.log(header(version) + ' ' + muted('No projects found.') + '\n');
66
119
  return;
@@ -72,7 +125,15 @@ export async function projectsList(options: { profile?: string } = {}) {
72
125
  : white(project.name),
73
126
  muted(project.id),
74
127
  muted(project.org_name || project.org_id),
75
- inactive(project.environment?.slice(0, 4) ?? 'dev'),
128
+ inactive(
129
+ project.environment === 'development'
130
+ ? 'dev'
131
+ : project.environment === 'production'
132
+ ? 'prod'
133
+ : project.environment === 'staging'
134
+ ? 'stg'
135
+ : project.environment ?? 'dev'
136
+ ),
76
137
  ]);
77
138
 
78
139
  console.log(header(version));
@@ -92,13 +153,19 @@ export async function projectsList(options: { profile?: string } = {}) {
92
153
  );
93
154
  }
94
155
 
95
- export async function projectsUse(projectId: string, options: { profile?: string } = {}) {
156
+ export async function projectsUse(
157
+ projectId: string,
158
+ options: { profile?: string; json?: boolean } = {}
159
+ ) {
96
160
  const profileName = resolveProfileName(options.profile);
97
161
  config.requireAuth(profileName);
98
162
 
99
163
  const projects = await manageRequest<ManageProject[]>('/projects', { profileName });
100
164
  const project = projects.find((item) => item.id === projectId);
101
165
  if (!project) {
166
+ if (options.json) {
167
+ jsonOutput({ success: false, error: `Project not found: ${projectId}` });
168
+ }
102
169
  console.log(failure(`Project not found: ${projectId}`));
103
170
  process.exit(1);
104
171
  }
@@ -114,10 +181,26 @@ export async function projectsUse(projectId: string, options: { profile?: string
114
181
  });
115
182
  config.setActiveProfile(profileName);
116
183
 
184
+ if (options.json) {
185
+ jsonOutput({
186
+ success: true,
187
+ project_id: project.id,
188
+ project_name: project.name,
189
+ });
190
+ }
191
+
117
192
  console.log(green('Active project: ') + `${project.name} (${project.id})`);
118
193
  }
119
194
 
120
- export async function projectsCreate(options: { profile?: string } = {}) {
195
+ export async function projectsCreate(
196
+ options: {
197
+ profile?: string;
198
+ name?: string;
199
+ org?: string;
200
+ env?: string;
201
+ json?: boolean;
202
+ } = {}
203
+ ) {
121
204
  const profileName = resolveProfileName(options.profile);
122
205
  config.requireAuth(profileName);
123
206
 
@@ -127,6 +210,51 @@ export async function projectsCreate(options: { profile?: string } = {}) {
127
210
  process.exit(1);
128
211
  }
129
212
 
213
+ const isNonInteractive = Boolean(options.name && options.org);
214
+
215
+ if (options.json && !isNonInteractive) {
216
+ jsonOutput({
217
+ success: false,
218
+ error: 'projects create --json requires --name <name> and --org <orgId>',
219
+ });
220
+ }
221
+
222
+ if (isNonInteractive) {
223
+ const org = orgs.find((item) => item.id === options.org);
224
+ if (!org) {
225
+ console.log(failure(`Organization not found: ${options.org}`));
226
+ process.exit(1);
227
+ }
228
+
229
+ const result = await createProjectRecord({
230
+ profileName,
231
+ orgId: org.id,
232
+ orgName: org.name,
233
+ name: options.name as string,
234
+ environment: options.env ?? 'development',
235
+ });
236
+
237
+ if (options.json) {
238
+ jsonOutput({
239
+ success: true,
240
+ project_id: result.project.id,
241
+ project_name: result.project.name,
242
+ org_id: org.id,
243
+ environment: result.project.environment,
244
+ client_key: result.keys.client,
245
+ server_key: result.keys.server,
246
+ });
247
+ }
248
+
249
+ console.log('');
250
+ console.log(green('Project created successfully.'));
251
+ console.log(orange('Project: ') + reset + `${result.project.name} (${result.project.id})`);
252
+ console.log(orange('Client key: ') + reset + result.keys.client);
253
+ console.log(orange('Server key: ') + reset + result.keys.server);
254
+ console.log('');
255
+ return;
256
+ }
257
+
130
258
  const orgId = await p.select({
131
259
  message: 'Select an organization',
132
260
  options: orgs.map((org) => ({
@@ -151,7 +279,7 @@ export async function projectsCreate(options: { profile?: string } = {}) {
151
279
  slug: ({ results }) =>
152
280
  p.text({
153
281
  message: 'Project slug',
154
- initialValue: slugify(String(results.name ?? '')),
282
+ initialValue: buildSlug(String(results.name ?? '')),
155
283
  validate: (value) => (!value ? 'Project slug is required' : undefined),
156
284
  }),
157
285
  environment: () =>
@@ -172,28 +300,15 @@ export async function projectsCreate(options: { profile?: string } = {}) {
172
300
  }
173
301
  );
174
302
 
175
- const result = await manageRequest<{
176
- project: { id: string; name: string; slug: string; environment: string; active: boolean };
177
- keys: { client: string; server: string };
178
- }>('/projects', {
179
- method: 'POST',
180
- body: {
181
- org_id: orgId,
182
- name: values.name,
183
- slug: values.slug,
184
- environment: values.environment,
185
- },
303
+ const result = await createProjectRecord({
186
304
  profileName,
305
+ orgId,
306
+ orgName: orgs.find((org) => org.id === orgId)?.name,
307
+ name: String(values.name),
308
+ slug: String(values.slug),
309
+ environment: String(values.environment),
187
310
  });
188
311
 
189
- config.setProfile(profileName, {
190
- active_project_id: result.project.id,
191
- active_project_name: result.project.name,
192
- org_name: orgs.find((org) => org.id === orgId)?.name,
193
- project_api_key: result.keys.server,
194
- });
195
- config.setActiveProfile(profileName);
196
-
197
312
  console.log('');
198
313
  console.log(green('Project created successfully.'));
199
314
  console.log(orange('Project: ') + reset + `${result.project.name} (${result.project.id})`);
@@ -201,3 +316,5 @@ export async function projectsCreate(options: { profile?: string } = {}) {
201
316
  console.log(orange('Server key: ') + reset + result.keys.server);
202
317
  console.log('');
203
318
  }
319
+
320
+ export { createProjectRecord, buildSlug };
@@ -6,6 +6,7 @@ import {
6
6
  green,
7
7
  header,
8
8
  inactive,
9
+ jsonOutput,
9
10
  muted,
10
11
  orange,
11
12
  renderTable,
@@ -26,7 +27,7 @@ const SERVICE_DESCRIPTIONS: Record<string, string> = {
26
27
  code: 'Edge functions and GC Hooks',
27
28
  };
28
29
 
29
- export async function servicesList(options: { profile?: string } = {}) {
30
+ export async function servicesList(options: { profile?: string; json?: boolean } = {}) {
30
31
  const profileName = options.profile ?? config.getActiveProfile() ?? 'default';
31
32
  const profile = config.getProfile(profileName);
32
33
  let serviceStatuses: ManageProjectServices = {};
@@ -42,6 +43,15 @@ export async function servicesList(options: { profile?: string } = {}) {
42
43
  }
43
44
  }
44
45
 
46
+ if (options.json) {
47
+ jsonOutput(
48
+ Object.keys(SERVICE_DESCRIPTIONS).map((service) => ({
49
+ service,
50
+ enabled: serviceStatuses[service] ?? false,
51
+ }))
52
+ );
53
+ }
54
+
45
55
  const rows = Object.entries(SERVICE_DESCRIPTIONS).map(([slug, desc]) => {
46
56
  const enabled = serviceStatuses[slug] ?? null;
47
57
  return [