@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 CHANGED
@@ -206,10 +206,10 @@ function renderTable(options) {
206
206
  );
207
207
  return lines.join("\n");
208
208
  }
209
- function header(version10, subtitle) {
209
+ function header(version11, subtitle) {
210
210
  const lines = [
211
211
  "",
212
- orange(" \u21D2\u21D2") + reset + " globio " + dim(version10),
212
+ orange(" \u21D2\u21D2") + reset + " globio " + dim(version11),
213
213
  dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")
214
214
  ];
215
215
  if (subtitle) {
@@ -221,6 +221,10 @@ function header(version10, subtitle) {
221
221
  function footer(text4) {
222
222
  return "\n" + dim(" " + text4) + "\n";
223
223
  }
224
+ function jsonOutput(data) {
225
+ console.log(JSON.stringify(data, null, 2));
226
+ process.exit(0);
227
+ }
224
228
 
225
229
  // src/lib/banner.ts
226
230
  var globioGradient = gradientString(
@@ -230,14 +234,14 @@ var globioGradient = gradientString(
230
234
  "#ffba08",
231
235
  "#ffd000"
232
236
  );
233
- function printBanner(version10) {
237
+ function printBanner(version11) {
234
238
  const art = figlet.textSync("Globio", {
235
239
  font: "ANSI Shadow",
236
240
  horizontalLayout: "default"
237
241
  });
238
242
  console.log(globioGradient.multiline(art));
239
243
  console.log(
240
- globioGradient(" \u21D2\u21D2") + " Game Backend as a Service \x1B[2mv" + version10 + "\x1B[0m"
244
+ globioGradient(" \u21D2\u21D2") + " Game Backend as a Service \x1B[2mv" + version11 + "\x1B[0m"
241
245
  );
242
246
  console.log("");
243
247
  }
@@ -262,6 +266,18 @@ async function savePat(token) {
262
266
  const account = await manageRequest("/account", { token });
263
267
  return account;
264
268
  }
269
+ function storeProfile(profileName, token, account) {
270
+ const hadProfiles = config.listProfiles().length > 0;
271
+ config.setProfile(profileName, {
272
+ pat: token,
273
+ account_email: account.email,
274
+ account_name: account.display_name ?? account.email,
275
+ created_at: Date.now()
276
+ });
277
+ if (profileName === "default" || !hadProfiles) {
278
+ config.setActiveProfile(profileName);
279
+ }
280
+ }
265
281
  function warnOnDuplicateAccount(accountEmail, targetProfileName) {
266
282
  const allProfiles = config.listProfiles();
267
283
  const duplicate = allProfiles.find((name) => {
@@ -275,8 +291,23 @@ function warnOnDuplicateAccount(accountEmail, targetProfileName) {
275
291
  );
276
292
  console.log("");
277
293
  }
278
- async function runTokenLogin(profileName) {
279
- const hadProfiles = config.listProfiles().length > 0;
294
+ async function completeTokenLogin(token, profileName, json = false) {
295
+ const account = await savePat(token);
296
+ if (!json) {
297
+ warnOnDuplicateAccount(account.email, profileName);
298
+ }
299
+ storeProfile(profileName, token, account);
300
+ if (json) {
301
+ jsonOutput({
302
+ success: true,
303
+ email: account.email,
304
+ name: account.display_name ?? account.email,
305
+ profile: profileName
306
+ });
307
+ }
308
+ return account;
309
+ }
310
+ async function runTokenLogin(profileName, json = false) {
280
311
  const token = await p.text({
281
312
  message: "Paste your personal access token",
282
313
  placeholder: "glo_pat_...",
@@ -293,17 +324,7 @@ async function runTokenLogin(profileName) {
293
324
  const spinner2 = p.spinner();
294
325
  spinner2.start("Validating personal access token...");
295
326
  try {
296
- const account = await savePat(token);
297
- warnOnDuplicateAccount(account.email, profileName);
298
- config.setProfile(profileName, {
299
- pat: token,
300
- account_email: account.email,
301
- account_name: account.display_name ?? account.email,
302
- created_at: Date.now()
303
- });
304
- if (profileName === "default" || !hadProfiles) {
305
- config.setActiveProfile(profileName);
306
- }
327
+ const account = await completeTokenLogin(token, profileName, json);
307
328
  spinner2.stop("Token validated.");
308
329
  p.outro(`Logged in as ${account.email}
309
330
  Profile: ${profileName}`);
@@ -313,10 +334,9 @@ Profile: ${profileName}`);
313
334
  process.exit(1);
314
335
  }
315
336
  }
316
- async function runBrowserLogin(profileName) {
337
+ async function runBrowserLogin(profileName, json = false) {
317
338
  const state = crypto.randomUUID();
318
339
  const spinner2 = p.spinner();
319
- const hadProfiles = config.listProfiles().length > 0;
320
340
  await manageRequest("/cli-auth/request", {
321
341
  method: "POST",
322
342
  body: { state }
@@ -342,16 +362,26 @@ async function runBrowserLogin(profileName) {
342
362
  method: "POST",
343
363
  body: { code: status.code }
344
364
  });
345
- warnOnDuplicateAccount(exchange.account.email, profileName);
365
+ if (!json) {
366
+ warnOnDuplicateAccount(exchange.account.email, profileName);
367
+ }
346
368
  config.setProfile(profileName, {
347
369
  pat: exchange.token,
348
370
  account_email: exchange.account.email,
349
371
  account_name: exchange.account.display_name ?? exchange.account.email,
350
372
  created_at: Date.now()
351
373
  });
352
- if (profileName === "default" || !hadProfiles) {
374
+ if (profileName === "default" || config.listProfiles().length === 1) {
353
375
  config.setActiveProfile(profileName);
354
376
  }
377
+ if (json) {
378
+ jsonOutput({
379
+ success: true,
380
+ email: exchange.account.email,
381
+ name: exchange.account.display_name ?? exchange.account.email,
382
+ profile: profileName
383
+ });
384
+ }
355
385
  spinner2.stop("Browser approval received.");
356
386
  p.outro(`Logged in as ${exchange.account.email}
357
387
  Profile: ${profileName}`);
@@ -366,9 +396,34 @@ Profile: ${profileName}`);
366
396
  process.exit(1);
367
397
  }
368
398
  async function login(options = {}) {
369
- printBanner(version);
370
399
  const profileName = options.profile ?? "default";
371
400
  const existing = config.getProfile(profileName);
401
+ if (options.token) {
402
+ try {
403
+ const account = await completeTokenLogin(options.token, profileName, options.json);
404
+ if (!options.json) {
405
+ console.log(`Logged in as ${account.email}
406
+ Profile: ${profileName}`);
407
+ }
408
+ return;
409
+ } catch (error) {
410
+ if (options.json) {
411
+ jsonOutput({
412
+ success: false,
413
+ error: error instanceof Error ? error.message : "Could not validate token"
414
+ });
415
+ }
416
+ console.log(failure(error instanceof Error ? error.message : "Could not validate token"));
417
+ process.exit(1);
418
+ }
419
+ }
420
+ if (options.json) {
421
+ jsonOutput({
422
+ success: false,
423
+ error: "login --json requires --token <pat>"
424
+ });
425
+ }
426
+ printBanner(version);
372
427
  if (existing) {
373
428
  const proceed = await p.confirm({
374
429
  message: `Already logged in as ${existing.account_email} on profile "${profileName}". Replace?`,
@@ -379,10 +434,6 @@ async function login(options = {}) {
379
434
  return;
380
435
  }
381
436
  }
382
- if (options.token) {
383
- await runTokenLogin(profileName);
384
- return;
385
- }
386
437
  const choice = await p.select({
387
438
  message: "Choose a login method",
388
439
  options: [
@@ -395,11 +446,11 @@ async function login(options = {}) {
395
446
  process.exit(0);
396
447
  }
397
448
  if (choice === "token") {
398
- await runTokenLogin(profileName);
449
+ await runTokenLogin(profileName, options.json);
399
450
  return;
400
451
  }
401
452
  try {
402
- await runBrowserLogin(profileName);
453
+ await runBrowserLogin(profileName, options.json);
403
454
  } catch (error) {
404
455
  p.outro(failure(error instanceof Error ? error.message : "Could not connect to Globio.") + "\x1B[0m");
405
456
  process.exit(1);
@@ -453,14 +504,36 @@ var version2 = getCliVersion();
453
504
  async function whoami(options = {}) {
454
505
  const profileName = options.profile ?? config.getActiveProfile() ?? "default";
455
506
  const profile = config.getProfile(profileName);
507
+ const active = config.getActiveProfile();
456
508
  if (!profile) {
509
+ if (options.json) {
510
+ jsonOutput({
511
+ profile: profileName,
512
+ active: false,
513
+ account_email: null,
514
+ account_name: null,
515
+ org_name: null,
516
+ active_project_id: null,
517
+ active_project_name: null
518
+ });
519
+ }
457
520
  console.log(
458
521
  header(version2) + " " + failure("Not logged in.") + reset + " Run: globio login\n"
459
522
  );
460
523
  return;
461
524
  }
525
+ if (options.json) {
526
+ jsonOutput({
527
+ profile: profileName,
528
+ active: profileName === active,
529
+ account_email: profile.account_email,
530
+ account_name: profile.account_name,
531
+ org_name: profile.org_name ?? null,
532
+ active_project_id: profile.active_project_id ?? null,
533
+ active_project_name: profile.active_project_name ?? null
534
+ });
535
+ }
462
536
  const allProfiles = config.listProfiles();
463
- const active = config.getActiveProfile();
464
537
  const otherProfiles = allProfiles.filter((p6) => p6 !== profileName).join(", ") || "\u2014";
465
538
  console.log(
466
539
  header(
@@ -607,33 +680,40 @@ function resolveProfileName(profile) {
607
680
  return profile ?? config.getActiveProfile() ?? "default";
608
681
  }
609
682
  async function migrateFirestore(options) {
610
- printBanner(version3);
611
- p3.intro(gold("\u21D2\u21D2") + " Firebase \u2192 Globio Migration");
683
+ if (!options.json) {
684
+ printBanner(version3);
685
+ p3.intro(gold("\u21D2\u21D2") + " Firebase \u2192 Globio Migration");
686
+ }
612
687
  const { firestore } = await initFirebase(options.from);
613
688
  const profileName = resolveProfileName(options.profile);
614
689
  let collections = [];
615
690
  if (options.all) {
616
691
  const snapshot = await firestore.listCollections();
617
692
  collections = snapshot.map((collection) => collection.id);
618
- console.log(
619
- green(
620
- `Found ${collections.length} collections: ${collections.join(", ")}`
621
- )
622
- );
693
+ if (!options.json) {
694
+ console.log(
695
+ green(`Found ${collections.length} collections: ${collections.join(", ")}`)
696
+ );
697
+ }
623
698
  } else if (options.collection) {
624
699
  collections = [options.collection];
625
700
  } else {
701
+ if (options.json) {
702
+ jsonOutput({ success: false, error: "Specify --collection <name> or --all" });
703
+ }
626
704
  console.log(failure("Specify --collection <name> or --all"));
627
705
  process.exit(1);
628
706
  }
629
707
  const results = {};
630
708
  for (const collectionId of collections) {
631
- console.log("");
632
- console.log(" " + orange(collectionId));
709
+ if (!options.json) {
710
+ console.log("");
711
+ console.log(" " + orange(collectionId));
712
+ }
633
713
  const countSnap = await firestore.collection(collectionId).count().get();
634
714
  const total = countSnap.data().count;
635
- const bar = createProgressBar(collectionId);
636
- bar.start(total, 0);
715
+ const bar = options.json ? null : createProgressBar(collectionId);
716
+ bar?.start(total, 0);
637
717
  results[collectionId] = {
638
718
  success: 0,
639
719
  failed: 0,
@@ -669,36 +749,49 @@ async function migrateFirestore(options) {
669
749
  results[collectionId].failedIds.push(doc.id);
670
750
  }
671
751
  processed++;
672
- bar.update(processed);
752
+ bar?.update(processed);
673
753
  }
674
754
  lastDoc = snapshot.docs[snapshot.docs.length - 1] ?? null;
675
755
  }
676
- bar.stop();
677
- console.log(
678
- green(` \u2713 ${results[collectionId].success} documents migrated`)
679
- );
680
- if (indexFieldCount > 0) {
681
- console.log(
682
- muted(` Indexes created for ${indexFieldCount} fields`)
683
- );
684
- }
685
- if (results[collectionId].failed > 0) {
686
- console.log(failure(` \u2717 ${results[collectionId].failed} failed`) + "\x1B[0m");
687
- console.log(
688
- muted(
689
- " Failed IDs: " + results[collectionId].failedIds.slice(0, 10).join(", ") + (results[collectionId].failedIds.length > 10 ? "..." : "")
690
- )
691
- );
756
+ bar?.stop();
757
+ if (!options.json) {
758
+ console.log(green(` \u2713 ${results[collectionId].success} documents migrated`));
759
+ if (indexFieldCount > 0) {
760
+ console.log(muted(` Indexes created for ${indexFieldCount} fields`));
761
+ }
762
+ if (results[collectionId].failed > 0) {
763
+ console.log(failure(` \u2717 ${results[collectionId].failed} failed`) + "\x1B[0m");
764
+ console.log(
765
+ muted(
766
+ " Failed IDs: " + results[collectionId].failedIds.slice(0, 10).join(", ") + (results[collectionId].failedIds.length > 10 ? "..." : "")
767
+ )
768
+ );
769
+ }
692
770
  }
693
771
  }
772
+ const summary = {
773
+ collections: collections.map((name) => ({
774
+ name,
775
+ migrated: results[name]?.success ?? 0,
776
+ failed: results[name]?.failed ?? 0,
777
+ failed_ids: results[name]?.failedIds ?? []
778
+ })),
779
+ total_migrated: collections.reduce((sum, name) => sum + (results[name]?.success ?? 0), 0),
780
+ total_failed: collections.reduce((sum, name) => sum + (results[name]?.failed ?? 0), 0)
781
+ };
782
+ if (options.json) {
783
+ jsonOutput(summary);
784
+ }
694
785
  console.log("");
695
786
  p3.outro(
696
787
  orange("\u2713") + " Migration complete.\n\n " + muted("Your Firebase data is intact.") + "\n " + muted("Delete it manually when ready.")
697
788
  );
698
789
  }
699
790
  async function migrateFirebaseStorage(options) {
700
- printBanner(version3);
701
- p3.intro(gold("\u21D2\u21D2") + " Firebase \u2192 Globio Migration");
791
+ if (!options.json) {
792
+ printBanner(version3);
793
+ p3.intro(gold("\u21D2\u21D2") + " Firebase \u2192 Globio Migration");
794
+ }
702
795
  const { storage } = await initFirebase(options.from);
703
796
  const profileName = resolveProfileName(options.profile);
704
797
  const profile = config.getProfile(profileName);
@@ -709,9 +802,11 @@ async function migrateFirebaseStorage(options) {
709
802
  const bucket = storage.bucket(bucketName);
710
803
  const prefix = options.folder ? options.folder.replace(/^\//, "") : "";
711
804
  const [files] = await bucket.getFiles(prefix ? { prefix } : {});
712
- console.log(green(`Found ${files.length} files to migrate`));
713
- const bar = createProgressBar("Storage");
714
- bar.start(files.length, 0);
805
+ if (!options.json) {
806
+ console.log(green(`Found ${files.length} files to migrate`));
807
+ }
808
+ const bar = options.json ? null : createProgressBar("Storage");
809
+ bar?.start(files.length, 0);
715
810
  let success = 0;
716
811
  let failed = 0;
717
812
  for (const file of files) {
@@ -725,16 +820,13 @@ async function migrateFirebaseStorage(options) {
725
820
  basename(file.name) || file.name
726
821
  );
727
822
  formData.append("path", file.name);
728
- const res = await fetch(
729
- "https://api.globio.stanlink.online/vault/files",
730
- {
731
- method: "POST",
732
- headers: {
733
- "X-Globio-Key": profile.project_api_key
734
- },
735
- body: formData
736
- }
737
- );
823
+ const res = await fetch("https://api.globio.stanlink.online/vault/files", {
824
+ method: "POST",
825
+ headers: {
826
+ "X-Globio-Key": profile.project_api_key
827
+ },
828
+ body: formData
829
+ });
738
830
  if (!res.ok) {
739
831
  throw new Error(`Upload failed: ${res.status}`);
740
832
  }
@@ -742,9 +834,16 @@ async function migrateFirebaseStorage(options) {
742
834
  } catch {
743
835
  failed++;
744
836
  }
745
- bar.increment();
837
+ bar?.increment();
838
+ }
839
+ bar?.stop();
840
+ const summary = {
841
+ migrated: success,
842
+ failed
843
+ };
844
+ if (options.json) {
845
+ jsonOutput(summary);
746
846
  }
747
- bar.stop();
748
847
  console.log("");
749
848
  console.log(green(` \u2713 ${success} files migrated`));
750
849
  if (failed > 0) {
@@ -778,11 +877,46 @@ async function createServerKey(projectId, profileName) {
778
877
  }
779
878
  return created.token;
780
879
  }
880
+ function buildSlug(value) {
881
+ return slugify(value).slice(0, 30);
882
+ }
883
+ async function createProjectRecord(input) {
884
+ const result = await manageRequest("/projects", {
885
+ method: "POST",
886
+ body: {
887
+ org_id: input.orgId,
888
+ name: input.name,
889
+ slug: input.slug ?? buildSlug(input.name),
890
+ environment: input.environment ?? "development"
891
+ },
892
+ profileName: input.profileName
893
+ });
894
+ config.setProfile(input.profileName, {
895
+ active_project_id: result.project.id,
896
+ active_project_name: result.project.name,
897
+ org_name: input.orgName,
898
+ project_api_key: result.keys.server
899
+ });
900
+ config.setActiveProfile(input.profileName);
901
+ return result;
902
+ }
781
903
  async function projectsList(options = {}) {
782
904
  const profileName = resolveProfileName2(options.profile);
783
905
  config.requireAuth(profileName);
784
906
  const projects2 = await manageRequest("/projects", { profileName });
785
907
  const activeProjectId = config.getProfile(profileName)?.active_project_id;
908
+ if (options.json) {
909
+ jsonOutput(
910
+ projects2.map((project) => ({
911
+ id: project.id,
912
+ name: project.name,
913
+ org_id: project.org_id,
914
+ org_name: project.org_name,
915
+ environment: project.environment,
916
+ active: project.id === activeProjectId
917
+ }))
918
+ );
919
+ }
786
920
  if (!projects2.length) {
787
921
  console.log(header(version4) + " " + muted("No projects found.") + "\n");
788
922
  return;
@@ -791,7 +925,9 @@ async function projectsList(options = {}) {
791
925
  activeProjectId === project.id ? gold(project.name) + reset + " " + orange("\u25CF") + reset : white(project.name),
792
926
  muted(project.id),
793
927
  muted(project.org_name || project.org_id),
794
- inactive(project.environment?.slice(0, 4) ?? "dev")
928
+ inactive(
929
+ project.environment === "development" ? "dev" : project.environment === "production" ? "prod" : project.environment === "staging" ? "stg" : project.environment ?? "dev"
930
+ )
795
931
  ]);
796
932
  console.log(header(version4));
797
933
  console.log(
@@ -815,6 +951,9 @@ async function projectsUse(projectId, options = {}) {
815
951
  const projects2 = await manageRequest("/projects", { profileName });
816
952
  const project = projects2.find((item) => item.id === projectId);
817
953
  if (!project) {
954
+ if (options.json) {
955
+ jsonOutput({ success: false, error: `Project not found: ${projectId}` });
956
+ }
818
957
  console.log(failure(`Project not found: ${projectId}`));
819
958
  process.exit(1);
820
959
  }
@@ -827,6 +966,13 @@ async function projectsUse(projectId, options = {}) {
827
966
  project_api_key: apiKey
828
967
  });
829
968
  config.setActiveProfile(profileName);
969
+ if (options.json) {
970
+ jsonOutput({
971
+ success: true,
972
+ project_id: project.id,
973
+ project_name: project.name
974
+ });
975
+ }
830
976
  console.log(green("Active project: ") + `${project.name} (${project.id})`);
831
977
  }
832
978
  async function projectsCreate(options = {}) {
@@ -837,6 +983,45 @@ async function projectsCreate(options = {}) {
837
983
  console.log(failure("No organizations found. Create one in the console first."));
838
984
  process.exit(1);
839
985
  }
986
+ const isNonInteractive = Boolean(options.name && options.org);
987
+ if (options.json && !isNonInteractive) {
988
+ jsonOutput({
989
+ success: false,
990
+ error: "projects create --json requires --name <name> and --org <orgId>"
991
+ });
992
+ }
993
+ if (isNonInteractive) {
994
+ const org = orgs.find((item) => item.id === options.org);
995
+ if (!org) {
996
+ console.log(failure(`Organization not found: ${options.org}`));
997
+ process.exit(1);
998
+ }
999
+ const result2 = await createProjectRecord({
1000
+ profileName,
1001
+ orgId: org.id,
1002
+ orgName: org.name,
1003
+ name: options.name,
1004
+ environment: options.env ?? "development"
1005
+ });
1006
+ if (options.json) {
1007
+ jsonOutput({
1008
+ success: true,
1009
+ project_id: result2.project.id,
1010
+ project_name: result2.project.name,
1011
+ org_id: org.id,
1012
+ environment: result2.project.environment,
1013
+ client_key: result2.keys.client,
1014
+ server_key: result2.keys.server
1015
+ });
1016
+ }
1017
+ console.log("");
1018
+ console.log(green("Project created successfully."));
1019
+ console.log(orange("Project: ") + reset + `${result2.project.name} (${result2.project.id})`);
1020
+ console.log(orange("Client key: ") + reset + result2.keys.client);
1021
+ console.log(orange("Server key: ") + reset + result2.keys.server);
1022
+ console.log("");
1023
+ return;
1024
+ }
840
1025
  const orgId = await p4.select({
841
1026
  message: "Select an organization",
842
1027
  options: orgs.map((org) => ({
@@ -857,7 +1042,7 @@ async function projectsCreate(options = {}) {
857
1042
  }),
858
1043
  slug: ({ results }) => p4.text({
859
1044
  message: "Project slug",
860
- initialValue: slugify(String(results.name ?? "")),
1045
+ initialValue: buildSlug(String(results.name ?? "")),
861
1046
  validate: (value) => !value ? "Project slug is required" : void 0
862
1047
  }),
863
1048
  environment: () => p4.select({
@@ -876,23 +1061,14 @@ async function projectsCreate(options = {}) {
876
1061
  }
877
1062
  }
878
1063
  );
879
- const result = await manageRequest("/projects", {
880
- method: "POST",
881
- body: {
882
- org_id: orgId,
883
- name: values.name,
884
- slug: values.slug,
885
- environment: values.environment
886
- },
887
- profileName
1064
+ const result = await createProjectRecord({
1065
+ profileName,
1066
+ orgId,
1067
+ orgName: orgs.find((org) => org.id === orgId)?.name,
1068
+ name: String(values.name),
1069
+ slug: String(values.slug),
1070
+ environment: String(values.environment)
888
1071
  });
889
- config.setProfile(profileName, {
890
- active_project_id: result.project.id,
891
- active_project_name: result.project.name,
892
- org_name: orgs.find((org) => org.id === orgId)?.name,
893
- project_api_key: result.keys.server
894
- });
895
- config.setActiveProfile(profileName);
896
1072
  console.log("");
897
1073
  console.log(green("Project created successfully."));
898
1074
  console.log(orange("Project: ") + reset + `${result.project.name} (${result.project.id})`);
@@ -904,24 +1080,59 @@ async function projectsCreate(options = {}) {
904
1080
  // src/commands/init.ts
905
1081
  var version5 = getCliVersion();
906
1082
  async function init(options = {}) {
907
- printBanner(version5);
908
- p5.intro(orange("\u21D2\u21D2") + " Initialize your Globio project");
909
1083
  const profileName = options.profile ?? config.getActiveProfile() ?? "default";
910
1084
  const profile = config.getProfile(profileName);
911
1085
  if (!profile) {
1086
+ if (options.json) {
1087
+ jsonOutput({ success: false, error: `Run: npx @globio/cli login --profile ${profileName}` });
1088
+ }
912
1089
  console.log(failure("Run: npx @globio/cli login --profile " + profileName));
913
1090
  process.exit(1);
914
1091
  }
915
- if (!profile.active_project_id) {
916
- await projectsCreate({ profile: profileName });
1092
+ const isNonInteractive = Boolean(options.name && options.org);
1093
+ let filesCreated = [];
1094
+ if (options.json && !isNonInteractive) {
1095
+ jsonOutput({
1096
+ success: false,
1097
+ error: "init --json requires --name <name> and --org <orgId>"
1098
+ });
1099
+ }
1100
+ if (!isNonInteractive) {
1101
+ printBanner(version5);
1102
+ p5.intro(orange("\u21D2\u21D2") + " Initialize your Globio project");
1103
+ if (!profile.active_project_id) {
1104
+ await projectsCreate({ profile: profileName });
1105
+ } else {
1106
+ await projectsUse(profile.active_project_id, { profile: profileName });
1107
+ }
917
1108
  } else {
918
- await projectsUse(profile.active_project_id, { profile: profileName });
1109
+ const orgId = options.org;
1110
+ const orgName = profile.org_name;
1111
+ const created = await createProjectRecord({
1112
+ profileName,
1113
+ orgId,
1114
+ orgName,
1115
+ name: options.name,
1116
+ slug: options.slug ?? buildSlug(options.name),
1117
+ environment: options.env ?? "development"
1118
+ });
1119
+ void created;
919
1120
  }
920
- const values = await promptInit();
1121
+ const values = isNonInteractive ? {
1122
+ migrateFromFirebase: Boolean(options.from),
1123
+ serviceAccountPath: options.from
1124
+ } : await promptInit();
921
1125
  const activeProfile = config.getProfile(profileName);
922
1126
  const activeProjectKey = activeProfile?.project_api_key;
923
1127
  const { projectId: activeProjectId } = config.requireProject(profileName);
1128
+ const activeProjectName = activeProfile?.active_project_name ?? "unnamed";
924
1129
  if (!activeProjectKey) {
1130
+ if (options.json) {
1131
+ jsonOutput({
1132
+ success: false,
1133
+ error: `No project API key cached. Run: npx @globio/cli projects use ${activeProjectId}`
1134
+ });
1135
+ }
925
1136
  console.log(failure("No project API key cached. Run: npx @globio/cli projects use " + activeProjectId));
926
1137
  process.exit(1);
927
1138
  }
@@ -936,16 +1147,24 @@ export const globio = new Globio({
936
1147
  });
937
1148
  `
938
1149
  );
939
- printSuccess("Created globio.config.ts");
1150
+ filesCreated.push("globio.config.ts");
1151
+ if (!options.json) {
1152
+ printSuccess("Created globio.config.ts");
1153
+ }
940
1154
  }
941
1155
  if (!existsSync2(".env")) {
942
1156
  writeFileSync2(".env", `GLOBIO_API_KEY=${activeProjectKey}
943
1157
  `);
944
- printSuccess("Created .env");
1158
+ filesCreated.push(".env");
1159
+ if (!options.json) {
1160
+ printSuccess("Created .env");
1161
+ }
945
1162
  }
946
1163
  if (values.migrateFromFirebase && values.serviceAccountPath) {
947
- console.log("");
948
- printSuccess("Starting Firebase migration...");
1164
+ if (!options.json) {
1165
+ console.log("");
1166
+ printSuccess("Starting Firebase migration...");
1167
+ }
949
1168
  await migrateFirestore({
950
1169
  from: values.serviceAccountPath,
951
1170
  all: true,
@@ -961,6 +1180,15 @@ export const globio = new Globio({
961
1180
  profile: profileName
962
1181
  });
963
1182
  }
1183
+ if (options.json) {
1184
+ jsonOutput({
1185
+ success: true,
1186
+ project_id: activeProjectId,
1187
+ project_name: activeProjectName,
1188
+ api_key: activeProjectKey,
1189
+ files_created: filesCreated
1190
+ });
1191
+ }
964
1192
  console.log("");
965
1193
  p5.outro(
966
1194
  orange("\u21D2\u21D2") + " Your project is ready.\n\n " + muted("Next steps:") + `
@@ -973,9 +1201,22 @@ export const globio = new Globio({
973
1201
 
974
1202
  // src/commands/profiles.ts
975
1203
  var version6 = getCliVersion();
976
- async function profilesList() {
1204
+ async function profilesList(options = {}) {
977
1205
  const profiles2 = config.listProfiles();
978
1206
  const active = config.getActiveProfile();
1207
+ const wantsJson = options.json ?? process.argv.includes("--json");
1208
+ if (wantsJson) {
1209
+ jsonOutput(
1210
+ profiles2.map((name) => {
1211
+ const profile = config.getProfile(name);
1212
+ return {
1213
+ name,
1214
+ email: profile?.account_email ?? null,
1215
+ active: name === active
1216
+ };
1217
+ })
1218
+ );
1219
+ }
979
1220
  if (!profiles2.length) {
980
1221
  console.log(
981
1222
  header(version6) + " " + muted("No profiles. Run: globio login") + "\n"
@@ -1033,6 +1274,14 @@ async function servicesList(options = {}) {
1033
1274
  serviceStatuses = {};
1034
1275
  }
1035
1276
  }
1277
+ if (options.json) {
1278
+ jsonOutput(
1279
+ Object.keys(SERVICE_DESCRIPTIONS).map((service) => ({
1280
+ service,
1281
+ enabled: serviceStatuses[service] ?? false
1282
+ }))
1283
+ );
1284
+ }
1036
1285
  const rows = Object.entries(SERVICE_DESCRIPTIONS).map(([slug, desc]) => {
1037
1286
  const enabled = serviceStatuses[slug] ?? null;
1038
1287
  return [
@@ -1075,10 +1324,28 @@ var version8 = getCliVersion();
1075
1324
  function resolveProfileName3(profile) {
1076
1325
  return profile ?? config.getActiveProfile() ?? "default";
1077
1326
  }
1327
+ function parseJsonField(value) {
1328
+ if (!value) return null;
1329
+ try {
1330
+ return JSON.parse(value);
1331
+ } catch {
1332
+ return null;
1333
+ }
1334
+ }
1078
1335
  async function functionsList(options = {}) {
1079
1336
  const profileName = resolveProfileName3(options.profile);
1080
1337
  const client = getClient(profileName);
1081
1338
  const result = await client.code.listFunctions();
1339
+ if (options.json) {
1340
+ jsonOutput(
1341
+ result.success ? result.data.map((fn) => ({
1342
+ slug: fn.slug,
1343
+ type: fn.type,
1344
+ trigger_event: fn.trigger_event,
1345
+ active: fn.active
1346
+ })) : []
1347
+ );
1348
+ }
1082
1349
  if (!result.success || !result.data.length) {
1083
1350
  console.log(header(version8) + " " + muted("No functions found.") + "\n");
1084
1351
  return;
@@ -1103,9 +1370,12 @@ async function functionsList(options = {}) {
1103
1370
  );
1104
1371
  console.log("");
1105
1372
  }
1106
- async function functionsCreate(slug, _options = {}) {
1373
+ async function functionsCreate(slug, options = {}) {
1107
1374
  const filename = `${slug}.js`;
1108
1375
  if (existsSync3(filename)) {
1376
+ if (options.json) {
1377
+ jsonOutput({ success: false, file: filename, error: "File already exists" });
1378
+ }
1109
1379
  console.log(inactive(`${filename} already exists.`));
1110
1380
  return;
1111
1381
  }
@@ -1125,6 +1395,9 @@ async function handler(input, globio) {
1125
1395
  }
1126
1396
  `;
1127
1397
  writeFileSync3(filename, template);
1398
+ if (options.json) {
1399
+ jsonOutput({ success: true, file: filename });
1400
+ }
1128
1401
  console.log(green(`Created ${filename}`));
1129
1402
  console.log(muted(`Deploy with: npx @globio/cli functions deploy ${slug}`));
1130
1403
  }
@@ -1141,8 +1414,8 @@ async function functionsDeploy(slug, options) {
1141
1414
  const code = readFileSync4(filename, "utf-8");
1142
1415
  const profileName = resolveProfileName3(options.profile);
1143
1416
  const client = getClient(profileName);
1144
- const spinner2 = ora(`Deploying ${slug}...`).start();
1145
1417
  const existing = await client.code.getFunction(slug);
1418
+ const spinner2 = options.json ? null : ora(`Deploying ${slug}...`).start();
1146
1419
  let result;
1147
1420
  if (existing.success) {
1148
1421
  result = await client.code.updateFunction(slug, {
@@ -1158,11 +1431,21 @@ async function functionsDeploy(slug, options) {
1158
1431
  });
1159
1432
  }
1160
1433
  if (!result.success) {
1161
- spinner2.fail("Deploy failed");
1434
+ if (options.json) {
1435
+ jsonOutput({ success: false, error: result.error.message });
1436
+ }
1437
+ spinner2?.fail("Deploy failed");
1162
1438
  console.error(result.error.message);
1163
1439
  process.exit(1);
1164
1440
  }
1165
- spinner2.succeed(existing.success ? `Updated ${slug}` : `Deployed ${slug}`);
1441
+ if (options.json) {
1442
+ jsonOutput({
1443
+ success: true,
1444
+ slug,
1445
+ action: existing.success ? "updated" : "created"
1446
+ });
1447
+ }
1448
+ spinner2?.succeed(existing.success ? `Updated ${slug}` : `Deployed ${slug}`);
1166
1449
  }
1167
1450
  async function functionsInvoke(slug, options) {
1168
1451
  let input = {};
@@ -1176,14 +1459,23 @@ async function functionsInvoke(slug, options) {
1176
1459
  }
1177
1460
  const profileName = resolveProfileName3(options.profile);
1178
1461
  const client = getClient(profileName);
1179
- const spinner2 = ora(`Invoking ${slug}...`).start();
1462
+ const spinner2 = options.json ? null : ora(`Invoking ${slug}...`).start();
1180
1463
  const result = await client.code.invoke(slug, input);
1181
- spinner2.stop();
1464
+ spinner2?.stop();
1182
1465
  if (!result.success) {
1466
+ if (options.json) {
1467
+ jsonOutput({ success: false, error: result.error.message });
1468
+ }
1183
1469
  console.log(failure("Invocation failed"));
1184
1470
  console.error(result.error.message);
1185
1471
  return;
1186
1472
  }
1473
+ if (options.json) {
1474
+ jsonOutput({
1475
+ result: result.data.result,
1476
+ duration_ms: result.data.duration_ms
1477
+ });
1478
+ }
1187
1479
  console.log("");
1188
1480
  console.log(orange("Result:"));
1189
1481
  console.log(JSON.stringify(result.data.result, null, 2));
@@ -1195,6 +1487,21 @@ async function functionsLogs(slug, options) {
1195
1487
  const profileName = resolveProfileName3(options.profile);
1196
1488
  const client = getClient(profileName);
1197
1489
  const result = await client.code.getInvocations(slug, limit);
1490
+ if (options.json) {
1491
+ jsonOutput(
1492
+ result.success ? result.data.map((invocation) => ({
1493
+ id: invocation.id,
1494
+ trigger_type: invocation.trigger_type,
1495
+ duration_ms: invocation.duration_ms,
1496
+ success: invocation.success,
1497
+ invoked_at: invocation.invoked_at,
1498
+ logs: parseJsonField(invocation.logs) ?? [],
1499
+ error_message: invocation.error_message ?? null,
1500
+ input: parseJsonField(invocation.input),
1501
+ result: parseJsonField(invocation.result)
1502
+ })) : []
1503
+ );
1504
+ }
1198
1505
  if (!result.success || !result.data.length) {
1199
1506
  console.log(header(version8) + " " + muted("No invocations yet.") + "\n");
1200
1507
  return;
@@ -1225,35 +1532,152 @@ async function functionsLogs(slug, options) {
1225
1532
  async function functionsDelete(slug, options = {}) {
1226
1533
  const profileName = resolveProfileName3(options.profile);
1227
1534
  const client = getClient(profileName);
1228
- const spinner2 = ora(`Deleting ${slug}...`).start();
1535
+ const spinner2 = options.json ? null : ora(`Deleting ${slug}...`).start();
1229
1536
  const result = await client.code.deleteFunction(slug);
1230
1537
  if (!result.success) {
1231
- spinner2.fail(`Delete failed for ${slug}`);
1538
+ if (options.json) {
1539
+ jsonOutput({ success: false, error: result.error.message });
1540
+ }
1541
+ spinner2?.fail(`Delete failed for ${slug}`);
1232
1542
  console.error(result.error.message);
1233
1543
  process.exit(1);
1234
1544
  }
1235
- spinner2.succeed(`Deleted ${slug}`);
1545
+ if (options.json) {
1546
+ jsonOutput({ success: true, slug });
1547
+ }
1548
+ spinner2?.succeed(`Deleted ${slug}`);
1236
1549
  }
1237
1550
  async function functionsToggle(slug, active, options = {}) {
1238
1551
  const profileName = resolveProfileName3(options.profile);
1239
1552
  const client = getClient(profileName);
1240
- const spinner2 = ora(
1241
- `${active ? "Enabling" : "Disabling"} ${slug}...`
1242
- ).start();
1553
+ const spinner2 = options.json ? null : ora(`${active ? "Enabling" : "Disabling"} ${slug}...`).start();
1243
1554
  const result = await client.code.toggleFunction(slug, active);
1244
1555
  if (!result.success) {
1245
- spinner2.fail(`Toggle failed for ${slug}`);
1556
+ if (options.json) {
1557
+ jsonOutput({ success: false, error: result.error.message });
1558
+ }
1559
+ spinner2?.fail(`Toggle failed for ${slug}`);
1246
1560
  console.error(result.error.message);
1247
1561
  process.exit(1);
1248
1562
  }
1249
- spinner2.succeed(`${slug} is now ${active ? "active" : "inactive"}`);
1563
+ if (options.json) {
1564
+ jsonOutput({ success: true, slug, active });
1565
+ }
1566
+ spinner2?.succeed(`${slug} is now ${active ? "active" : "inactive"}`);
1250
1567
  }
1251
1568
 
1252
- // src/index.ts
1569
+ // src/commands/watch.ts
1570
+ var BASE_URL2 = "https://api.globio.stanlink.online";
1253
1571
  var version9 = getCliVersion();
1572
+ async function functionsWatch(slug, options = {}) {
1573
+ const profileName = options.profile ?? config.getActiveProfile();
1574
+ const profile = config.getProfile(profileName ?? "default");
1575
+ if (!profile?.project_api_key) {
1576
+ console.log(
1577
+ failure("No active project.") + reset + " Run: globio projects use <id>"
1578
+ );
1579
+ process.exit(1);
1580
+ }
1581
+ console.log(header(version9));
1582
+ console.log(
1583
+ " " + orange("watching") + reset + " " + slug + dim(" \xB7 press Ctrl+C to stop") + "\n"
1584
+ );
1585
+ const res = await fetch(`${BASE_URL2}/code/functions/${slug}/watch`, {
1586
+ headers: {
1587
+ "X-Globio-Key": profile.project_api_key,
1588
+ Accept: "text/event-stream"
1589
+ }
1590
+ });
1591
+ if (!res.ok || !res.body) {
1592
+ console.log(failure("Failed to connect to watch stream.") + reset);
1593
+ process.exit(1);
1594
+ }
1595
+ const reader = res.body.getReader();
1596
+ const decoder = new TextDecoder();
1597
+ let buffer = "";
1598
+ process.on("SIGINT", () => {
1599
+ console.log("\n" + dim(" Stream closed.") + "\n");
1600
+ void reader.cancel();
1601
+ process.exit(0);
1602
+ });
1603
+ while (true) {
1604
+ const { done, value } = await reader.read();
1605
+ if (done) break;
1606
+ buffer += decoder.decode(value, { stream: true });
1607
+ const chunks = buffer.split("\n\n");
1608
+ buffer = chunks.pop() ?? "";
1609
+ for (const chunk of chunks) {
1610
+ const dataLine = chunk.split("\n").find((line) => line.startsWith("data: "));
1611
+ if (!dataLine) continue;
1612
+ try {
1613
+ renderEvent(JSON.parse(dataLine.slice(6)));
1614
+ } catch {
1615
+ }
1616
+ }
1617
+ }
1618
+ }
1619
+ function renderEvent(event) {
1620
+ if (event.type === "connected") {
1621
+ console.log(
1622
+ " " + green("\u25CF") + reset + dim(" connected \u2014 waiting for invocations...\n")
1623
+ );
1624
+ return;
1625
+ }
1626
+ if (event.type === "heartbeat") {
1627
+ return;
1628
+ }
1629
+ if (event.type === "timeout") {
1630
+ console.log(
1631
+ "\n" + dim(" Session timed out after 5 minutes.") + " Run again to resume.\n"
1632
+ );
1633
+ return;
1634
+ }
1635
+ if (event.type !== "invocation" || !event.invoked_at) {
1636
+ return;
1637
+ }
1638
+ const time = new Date(event.invoked_at * 1e3).toISOString().replace("T", " ").slice(0, 19);
1639
+ const status = event.success ? green("\u2713") : failure("\u2717");
1640
+ const trigger = dim(`[${event.trigger_type ?? "http"}]`);
1641
+ const duration = dim(`${event.duration_ms ?? 0}ms`);
1642
+ console.log(
1643
+ " " + status + reset + " " + dim(time) + " " + trigger + " " + duration
1644
+ );
1645
+ if (event.input && event.input !== "{}") {
1646
+ try {
1647
+ console.log(
1648
+ " " + dim(" input ") + muted(JSON.stringify(JSON.parse(event.input)))
1649
+ );
1650
+ } catch {
1651
+ }
1652
+ }
1653
+ if (event.logs) {
1654
+ try {
1655
+ const logs = JSON.parse(event.logs);
1656
+ for (const line of logs) {
1657
+ console.log(" " + dim(" log ") + reset + line);
1658
+ }
1659
+ } catch {
1660
+ }
1661
+ }
1662
+ if (event.result && event.result !== "null") {
1663
+ try {
1664
+ console.log(
1665
+ " " + dim(" result ") + muted(JSON.stringify(JSON.parse(event.result)))
1666
+ );
1667
+ } catch {
1668
+ }
1669
+ }
1670
+ if (event.error_message) {
1671
+ console.log(" " + dim(" error ") + failure(event.error_message) + reset);
1672
+ }
1673
+ console.log("");
1674
+ }
1675
+
1676
+ // src/index.ts
1677
+ var version10 = getCliVersion();
1254
1678
  var program = new Command();
1255
- program.name("globio").description("The official Globio CLI").version(version9).addHelpText("beforeAll", () => {
1256
- printBanner(version9);
1679
+ program.name("globio").description("The official Globio CLI").version(version10).addHelpText("beforeAll", () => {
1680
+ printBanner(version10);
1257
1681
  return "";
1258
1682
  }).addHelpText(
1259
1683
  "after",
@@ -1270,30 +1694,35 @@ Examples:
1270
1694
  Credentials are stored in ~/.globio/profiles/
1271
1695
  `
1272
1696
  );
1273
- program.command("login").description("Log in to your Globio account").option("-p, --profile <name>", "Profile name", "default").option("--token", "Use a personal access token").action(login);
1697
+ program.command("login").description("Log in to your Globio account").option("-p, --profile <name>", "Profile name", "default").option("--token <pat>", "Personal access token (non-interactive)").option("--json", "Output as JSON").action(login);
1274
1698
  program.command("logout").description("Log out").option("--profile <name>", "Use a specific profile").action(logout);
1275
- program.command("whoami").description("Show current account and project").option("--profile <name>", "Use a specific profile").action(whoami);
1699
+ program.command("whoami").description("Show current account and project").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(whoami);
1276
1700
  program.command("use <profile>").description("Switch active profile").action(useProfile);
1277
- program.command("init").description("Initialize a Globio project").option("--profile <name>", "Use a specific profile").action(init);
1278
- var profiles = program.command("profiles").description("Manage login profiles").action(profilesList);
1279
- profiles.command("list").description("List all profiles").action(profilesList);
1701
+ program.command("init").description("Initialize a Globio project").option("-p, --profile <name>", "Profile name").option("--name <name>", "Project name").option("--slug <slug>", "Project slug").option("--org <orgId>", "Organization ID").option("--env <environment>", "Environment (development|staging|production)", "development").option("--no-migrate", "Skip Firebase migration prompt").option("--from <path>", "Firebase service account path (triggers migration)").option("--json", "Output as JSON").action(init);
1702
+ var profiles = program.command("profiles").description("Manage login profiles").option("--json", "Output as JSON").action(function() {
1703
+ return profilesList(this.opts());
1704
+ });
1705
+ profiles.command("list").description("List all profiles").option("--json", "Output as JSON").action(function() {
1706
+ return profilesList(this.opts());
1707
+ });
1280
1708
  var projects = program.command("projects").description("Manage projects");
1281
- projects.command("list").description("List projects").option("--profile <name>", "Use a specific profile").action(projectsList);
1282
- projects.command("create").description("Create a project").option("--profile <name>", "Use a specific profile").action(projectsCreate);
1283
- projects.command("use <projectId>").description("Set active project").option("--profile <name>", "Use a specific profile").action(projectsUse);
1284
- program.command("services").description("List available Globio services").option("--profile <name>", "Use a specific profile").action(servicesList);
1709
+ projects.command("list").description("List projects").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(projectsList);
1710
+ projects.command("create").description("Create a project").option("--name <name>", "Project name").option("--org <orgId>", "Organization ID").option("--env <environment>", "Environment", "development").option("-p, --profile <name>", "Profile name").option("--json", "Output as JSON").action(projectsCreate);
1711
+ projects.command("use <projectId>").description("Set active project").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(projectsUse);
1712
+ program.command("services").description("List available Globio services").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(servicesList);
1285
1713
  var functions = program.command("functions").alias("fn").description("Manage GlobalCode edge functions");
1286
- functions.command("list").description("List all functions").option("--profile <name>", "Use a specific profile").action(functionsList);
1287
- functions.command("create <slug>").description("Scaffold a new function file locally").option("--profile <name>", "Use a specific profile").action(functionsCreate);
1288
- functions.command("deploy <slug>").description("Deploy a function to GlobalCode").option("-f, --file <path>", "Path to function file").option("-n, --name <name>", "Display name").option("--profile <name>", "Use a specific profile").action(functionsDeploy);
1289
- functions.command("invoke <slug>").description("Invoke a function").option("-i, --input <json>", "JSON input payload").option("--profile <name>", "Use a specific profile").action(functionsInvoke);
1290
- functions.command("logs <slug>").description("Show invocation history").option("-l, --limit <n>", "Number of entries", "20").option("--profile <name>", "Use a specific profile").action(functionsLogs);
1291
- functions.command("delete <slug>").description("Delete a function").option("--profile <name>", "Use a specific profile").action(functionsDelete);
1292
- functions.command("enable <slug>").description("Enable a function").option("--profile <name>", "Use a specific profile").action((slug, options) => functionsToggle(slug, true, options));
1293
- functions.command("disable <slug>").description("Disable a function").option("--profile <name>", "Use a specific profile").action((slug, options) => functionsToggle(slug, false, options));
1714
+ functions.command("list").description("List all functions").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(functionsList);
1715
+ 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);
1716
+ functions.command("deploy <slug>").description("Deploy a function to GlobalCode").option("-f, --file <path>", "Path to function file").option("-n, --name <name>", "Display name").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(functionsDeploy);
1717
+ functions.command("invoke <slug>").description("Invoke a function").option("-i, --input <json>", "JSON input payload").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(functionsInvoke);
1718
+ functions.command("logs <slug>").description("Show invocation history").option("-l, --limit <n>", "Number of entries", "20").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(functionsLogs);
1719
+ functions.command("watch <slug>").description("Stream live function execution logs").option("--profile <name>", "Use a specific profile").action(functionsWatch);
1720
+ functions.command("delete <slug>").description("Delete a function").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(functionsDelete);
1721
+ functions.command("enable <slug>").description("Enable a function").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action((slug, options) => functionsToggle(slug, true, options));
1722
+ functions.command("disable <slug>").description("Disable a function").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action((slug, options) => functionsToggle(slug, false, options));
1294
1723
  var migrate = program.command("migrate").description("Migrate from Firebase to Globio");
1295
- migrate.command("firestore").description("Migrate Firestore collections to GlobalDoc").requiredOption("--from <path>", "Path to Firebase service account JSON").option("--collection <name>", "Migrate a specific collection").option("--all", "Migrate all collections").option("--profile <name>", "Use a specific profile").action(migrateFirestore);
1296
- migrate.command("firebase-storage").description("Migrate Firebase Storage to GlobalVault").requiredOption("--from <path>", "Path to Firebase service account JSON").requiredOption("--bucket <name>", "Firebase Storage bucket").option("--folder <path>", "Migrate a specific folder").option("--all", "Migrate all files").option("--profile <name>", "Use a specific profile").action(migrateFirebaseStorage);
1724
+ migrate.command("firestore").description("Migrate Firestore collections to GlobalDoc").requiredOption("--from <path>", "Path to Firebase service account JSON").option("--collection <name>", "Migrate a specific collection").option("--all", "Migrate all collections").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(migrateFirestore);
1725
+ migrate.command("firebase-storage").description("Migrate Firebase Storage to GlobalVault").requiredOption("--from <path>", "Path to Firebase service account JSON").requiredOption("--bucket <name>", "Firebase Storage bucket").option("--folder <path>", "Migrate a specific folder").option("--all", "Migrate all files").option("--profile <name>", "Use a specific profile").option("--json", "Output as JSON").action(migrateFirebaseStorage);
1297
1726
  async function main() {
1298
1727
  if (process.argv.length <= 2) {
1299
1728
  program.help();