@capgo/cli 4.8.0 → 4.8.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/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [4.8.2](https://github.com/Cap-go/CLI/compare/v4.8.1...v4.8.2) (2024-05-11)
6
+
7
+ ### [4.8.1](https://github.com/Cap-go/CLI/compare/v4.8.0...v4.8.1) (2024-05-11)
8
+
5
9
  ## [4.8.0](https://github.com/Cap-go/CLI/compare/v4.7.0...v4.8.0) (2024-05-10)
6
10
 
7
11
 
package/dist/index.js CHANGED
@@ -92373,7 +92373,7 @@ var {
92373
92373
  // package.json
92374
92374
  var package_default = {
92375
92375
  name: "@capgo/cli",
92376
- version: "4.8.0",
92376
+ version: "4.8.2",
92377
92377
  description: "A CLI to upload to capgo servers",
92378
92378
  author: "github.com/riderx",
92379
92379
  license: "Apache 2.0",
@@ -93916,7 +93916,8 @@ async function updateOrCreateVersion(supabase, update) {
93916
93916
  async function uploadUrl(supabase, appId, name) {
93917
93917
  const data = {
93918
93918
  app_id: appId,
93919
- name
93919
+ name,
93920
+ version: 0
93920
93921
  };
93921
93922
  try {
93922
93923
  const pathUploadLink = "private/upload_link";
@@ -93927,6 +93928,52 @@ async function uploadUrl(supabase, appId, name) {
93927
93928
  }
93928
93929
  return "";
93929
93930
  }
93931
+ async function prepareMultipart(supabase, appId, name) {
93932
+ const data = {
93933
+ app_id: appId,
93934
+ name,
93935
+ version: 1
93936
+ };
93937
+ try {
93938
+ const pathUploadLink = "private/upload_link";
93939
+ const res = await supabase.functions.invoke(pathUploadLink, { body: JSON.stringify(data) });
93940
+ return res.data;
93941
+ } catch (error) {
93942
+ f2.error(`Cannot get upload url ${formatError(error)}`);
93943
+ return null;
93944
+ }
93945
+ }
93946
+ async function finishMultipartDownload(key2, uploadId, url, parts) {
93947
+ const metadata = {
93948
+ action: "mpu-complete",
93949
+ uploadId,
93950
+ key: key2
93951
+ };
93952
+ await distribution_default.post(url, {
93953
+ json: {
93954
+ parts
93955
+ },
93956
+ searchParams: new URLSearchParams({ body: btoa(JSON.stringify(metadata)) })
93957
+ });
93958
+ }
93959
+ var PART_SIZE = 10 * 1024 * 1024;
93960
+ async function uploadMultipart(supabase, appId, name, data) {
93961
+ try {
93962
+ const multipartPrep = await prepareMultipart(supabase, appId, name);
93963
+ if (!multipartPrep) {
93964
+ return false;
93965
+ }
93966
+ const fileSize = data.length;
93967
+ const partCount = Math.ceil(fileSize / PART_SIZE);
93968
+ const uploadPromises = Array.from({ length: partCount }, (_3, index) => uploadPart(data, PART_SIZE, multipartPrep.url, multipartPrep.key, multipartPrep.uploadId, index));
93969
+ const parts = await Promise.all(uploadPromises);
93970
+ await finishMultipartDownload(multipartPrep.key, multipartPrep.uploadId, multipartPrep.url, parts);
93971
+ return true;
93972
+ } catch (e2) {
93973
+ f2.error(`Could not upload via multipart ${formatError(e2)}`);
93974
+ return false;
93975
+ }
93976
+ }
93930
93977
  async function deletedFailedVersion(supabase, appId, name) {
93931
93978
  const data = {
93932
93979
  app_id: appId,
@@ -93941,6 +93988,23 @@ async function deletedFailedVersion(supabase, appId, name) {
93941
93988
  return Promise.reject(new Error("Cannot delete failed version"));
93942
93989
  }
93943
93990
  }
93991
+ async function uploadPart(data, partsize, url, key2, uploadId, index) {
93992
+ const dataToUpload = data.subarray(
93993
+ partsize * index,
93994
+ partsize * (index + 1)
93995
+ );
93996
+ const metadata = {
93997
+ action: "mpu-uploadpart",
93998
+ uploadId,
93999
+ partNumber: index + 1,
94000
+ key: key2
94001
+ };
94002
+ const response = await distribution_default.put(url, {
94003
+ body: dataToUpload,
94004
+ searchParams: new URLSearchParams({ body: btoa(JSON.stringify(metadata)) })
94005
+ });
94006
+ return await response.json();
94007
+ }
93944
94008
  async function updateOrCreateChannel(supabase, update) {
93945
94009
  if (!update.app_id || !update.name || !update.created_by) {
93946
94010
  f2.error("missing app_id, name, or created_by");
@@ -93967,11 +94031,38 @@ async function updateOrCreateChannel(supabase, update) {
93967
94031
  }
93968
94032
  function useLogSnag() {
93969
94033
  const logsnag = new import_logsnag.LogSnag({
93970
- token: "c124f5e9d0ce5bdd14bbb48f815d5583",
93971
- project: "capgo"
94034
+ token: import_node_process8.default.env.CAPGO_LOGSNAG ?? "c124f5e9d0ce5bdd14bbb48f815d5583",
94035
+ project: import_node_process8.default.env.CAPGO_LOGSNAG_PROJECT ?? "capgo"
93972
94036
  });
93973
94037
  return logsnag;
93974
94038
  }
94039
+ async function getOrganization(supabase, roles) {
94040
+ const { error: orgError, data: allOrganizations } = await supabase.rpc("get_orgs_v5");
94041
+ if (orgError) {
94042
+ f2.error("Cannot get the list of organizations - exiting");
94043
+ f2.error(`Error ${JSON.stringify(orgError)}`);
94044
+ program.error("");
94045
+ }
94046
+ const adminOrgs = allOrganizations.filter((org) => !!roles.find((role) => role === org.role));
94047
+ if (adminOrgs.length === 0) {
94048
+ f2.error(`Could not get organization with roles: ${roles.join(" or ")} because the user does not have any org`);
94049
+ program.error("");
94050
+ }
94051
+ const organizationUidRaw = adminOrgs.length > 1 ? await ie({
94052
+ message: "Please pick the organization that you want to insert to",
94053
+ options: adminOrgs.map((org) => {
94054
+ return { value: org.gid, label: org.name };
94055
+ })
94056
+ }) : adminOrgs[0].gid;
94057
+ if (eD(organizationUidRaw)) {
94058
+ f2.error("Canceled organization selection, exiting");
94059
+ program.error("");
94060
+ }
94061
+ const organizationUid = organizationUidRaw;
94062
+ const organization = allOrganizations.find((org) => org.gid === organizationUid);
94063
+ f2.info(`Using the organization "${organization.name}" as the app owner`);
94064
+ return organization;
94065
+ }
93975
94066
  var convertAppName = (appName) => appName.replace(/\./g, "--");
93976
94067
  async function verifyUser(supabase, apikey, keymod = ["all"]) {
93977
94068
  await checkKey(supabase, apikey, keymod);
@@ -94337,18 +94428,18 @@ function wait2(ms) {
94337
94428
  setTimeout(resolve2, ms);
94338
94429
  });
94339
94430
  }
94340
- async function markSnag(channel2, userId, snag, event, icon = "\u2705") {
94431
+ async function markSnag(channel2, orgId, snag, event, icon = "\u2705") {
94341
94432
  await snag.track({
94342
94433
  channel: channel2,
94343
94434
  event,
94344
94435
  icon,
94345
- user_id: userId,
94436
+ user_id: orgId,
94346
94437
  notify: false
94347
94438
  }).catch();
94348
94439
  }
94349
- async function cancelCommand(channel2, command, userId, snag) {
94440
+ async function cancelCommand(channel2, command, orgId, snag) {
94350
94441
  if (eD(command)) {
94351
- await markSnag(channel2, userId, snag, "canceled", "\u{1F937}");
94442
+ await markSnag(channel2, orgId, snag, "canceled", "\u{1F937}");
94352
94443
  import_node_process10.default.exit();
94353
94444
  }
94354
94445
  }
@@ -94364,13 +94455,13 @@ async function getStats(supabase, query) {
94364
94455
  }
94365
94456
  return null;
94366
94457
  }
94367
- async function waitLog(channel2, supabase, appId, snag, userId, deviceId) {
94458
+ async function waitLog(channel2, supabase, appId, snag, orgId, deviceId) {
94368
94459
  let loop = true;
94369
94460
  let now = (/* @__PURE__ */ new Date()).toISOString();
94370
94461
  const appIdUrl = convertAppName(appId);
94371
94462
  const config = await getLocalConfig();
94372
94463
  const baseUrl = `${config.hostWeb}/app/p/${appIdUrl}`;
94373
- await markSnag(channel2, userId, snag, "Use waitlog");
94464
+ await markSnag(channel2, orgId, snag, "Use waitlog");
94374
94465
  const query = {
94375
94466
  appId,
94376
94467
  devicesId: deviceId ? [deviceId] : void 0,
@@ -94388,12 +94479,12 @@ async function waitLog(channel2, supabase, appId, snag, userId, deviceId) {
94388
94479
  f2.info(`Log from Device: ${data.device_id}`);
94389
94480
  if (data.action === "get") {
94390
94481
  f2.info("Update Sent your your device, wait until event download complete");
94391
- await markSnag(channel2, userId, snag, "done");
94482
+ await markSnag(channel2, orgId, snag, "done");
94392
94483
  } else if (data.action.startsWith("download_")) {
94393
94484
  const action = data.action.split("_")[1];
94394
94485
  if (action === "complete") {
94395
94486
  f2.info("Your bundle has been downloaded on your device, background the app now and open it again to see the update");
94396
- await markSnag(channel2, userId, snag, "downloaded");
94487
+ await markSnag(channel2, orgId, snag, "downloaded");
94397
94488
  } else if (action === "fail") {
94398
94489
  f2.error("Your bundle has failed to download on your device.");
94399
94490
  f2.error("Please check if you have network connection and try again");
@@ -94403,7 +94494,7 @@ async function waitLog(channel2, supabase, appId, snag, userId, deviceId) {
94403
94494
  } else if (data.action === "set") {
94404
94495
  f2.info("Your bundle has been set on your device \u2764\uFE0F");
94405
94496
  loop = false;
94406
- await markSnag(channel2, userId, snag, "set");
94497
+ await markSnag(channel2, orgId, snag, "set");
94407
94498
  return Promise.resolve(data);
94408
94499
  } else if (data.action === "NoChannelOrOverride") {
94409
94500
  f2.error(`No default channel or override (channel/device) found, please create it here ${baseUrl}`);
@@ -94471,12 +94562,13 @@ async function debugApp(appId, options) {
94471
94562
  const userId = await verifyUser(supabase, options.apikey);
94472
94563
  f2.info(`Getting active bundle in Capgo`);
94473
94564
  await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appId, 4 /* admin */);
94565
+ const orgId = await getOrganizationId(supabase, appId);
94474
94566
  const doRun = await se({ message: `Automatic check if update working in device ?` });
94475
94567
  await cancelCommand("debug", doRun, userId, snag);
94476
94568
  if (doRun) {
94477
94569
  f2.info(`Wait logs sent to Capgo from ${appId} device, Put the app in background and open it again.`);
94478
94570
  f2.info("Waiting...");
94479
- await waitLog("debug", supabase, appId, snag, userId, deviceId);
94571
+ await waitLog("debug", supabase, appId, snag, orgId, deviceId);
94480
94572
  $e(`Done \u2705`);
94481
94573
  } else {
94482
94574
  $e(`Canceled \u274C`);
@@ -95066,23 +95158,27 @@ The app size is ${mbSize} Mb, this may take a while to download for users
95066
95158
  if (!external && zipped) {
95067
95159
  const spinner = de();
95068
95160
  spinner.start(`Uploading Bundle`);
95069
- const url = await uploadUrl(supabase, appid, bundle2);
95070
- if (!url) {
95071
- f2.error(`Cannot get upload url`);
95072
- program.error("");
95073
- }
95074
95161
  const startTime = import_node_perf_hooks.performance.now();
95075
95162
  try {
95076
- await distribution_default.put(url, {
95077
- timeout: options.timeout || UPLOAD_TIMEOUT,
95078
- retry: 5,
95079
- body: zipped,
95080
- headers: !localS3 ? {
95081
- "Content-Type": "application/octet-stream",
95082
- "Cache-Control": "public, max-age=456789, immutable",
95083
- "x-amz-meta-crc32": checksum
95084
- } : void 0
95085
- });
95163
+ if (options.multipart !== void 0 && options.multipart) {
95164
+ await uploadMultipart(supabase, appid, bundle2, zipped);
95165
+ } else {
95166
+ const url = await uploadUrl(supabase, appid, bundle2);
95167
+ if (!url) {
95168
+ f2.error(`Cannot get upload url`);
95169
+ program.error("");
95170
+ }
95171
+ await distribution_default.put(url, {
95172
+ timeout: options.timeout || UPLOAD_TIMEOUT,
95173
+ retry: 5,
95174
+ body: zipped,
95175
+ headers: !localS3 ? {
95176
+ "Content-Type": "application/octet-stream",
95177
+ "Cache-Control": "public, max-age=456789, immutable",
95178
+ "x-amz-meta-crc32": checksum
95179
+ } : void 0
95180
+ });
95181
+ }
95086
95182
  } catch (errorUpload) {
95087
95183
  const endTime2 = import_node_perf_hooks.performance.now();
95088
95184
  const uploadTime2 = ((endTime2 - startTime) / 1e3).toFixed(2);
@@ -95358,13 +95454,15 @@ var src_default = new Mime_default(standard_default, other_default)._freeze();
95358
95454
 
95359
95455
  // src/app/add.ts
95360
95456
  async function addApp(appId, options, throwErr = true) {
95457
+ await addAppInternal(appId, options, void 0, throwErr);
95458
+ }
95459
+ async function addAppInternal(appId, options, organization, throwErr = true) {
95361
95460
  if (throwErr)
95362
95461
  oe(`Adding`);
95363
95462
  await checkLatest();
95364
95463
  options.apikey = options.apikey || findSavedKey();
95365
95464
  const config = await getConfig();
95366
95465
  appId = appId || config?.app?.appId;
95367
- const snag = useLogSnag();
95368
95466
  if (!options.apikey) {
95369
95467
  f2.error(`Missing API key, you need to provide a API key to upload your bundle`);
95370
95468
  program.error("");
@@ -95378,7 +95476,7 @@ async function addApp(appId, options, throwErr = true) {
95378
95476
  program.error("");
95379
95477
  }
95380
95478
  const supabase = await createSupabaseClient(options.apikey);
95381
- let userId = await verifyUser(supabase, options.apikey, ["write", "all"]);
95479
+ const userId = await verifyUser(supabase, options.apikey, ["write", "all"]);
95382
95480
  const appExist = await checkAppExists(supabase, appId);
95383
95481
  if (throwErr && appExist) {
95384
95482
  f2.error(`App ${appId} already exist`);
@@ -95386,27 +95484,9 @@ async function addApp(appId, options, throwErr = true) {
95386
95484
  } else if (appExist) {
95387
95485
  return false;
95388
95486
  }
95389
- const { error: orgError, data: allOrganizations } = await supabase.rpc("get_orgs_v5");
95390
- if (orgError) {
95391
- f2.error("Cannot get the list of organizations - exiting");
95392
- f2.error(`Error ${JSON.stringify(orgError)}`);
95393
- program.error("");
95394
- }
95395
- const adminOrgs = allOrganizations.filter((org) => org.role === "admin" || org.role === "super_admin");
95396
- const organizationUidRaw = adminOrgs.length > 1 ? await ie({
95397
- message: "Please pick the organization that you want to insert to",
95398
- options: adminOrgs.map((org) => {
95399
- return { value: org.gid, label: org.name };
95400
- })
95401
- }) : adminOrgs[0].gid;
95402
- if (eD(organizationUidRaw)) {
95403
- f2.error("Canceled organization selection, exiting");
95404
- program.error("");
95405
- }
95406
- const organizationUid = organizationUidRaw;
95407
- const organization = allOrganizations.find((org) => org.gid === organizationUid);
95408
- userId = organization.created_by;
95409
- f2.info(`Using the organization "${organization.name}" as the app owner`);
95487
+ if (!organization)
95488
+ organization = await getOrganization(supabase, ["admin", "super_admin"]);
95489
+ const organizationUid = organization.gid;
95410
95490
  await checkPlanValid(supabase, organizationUid, options.apikey, void 0, false);
95411
95491
  let { name, icon } = options;
95412
95492
  appId = appId || config?.app?.appId;
@@ -95471,16 +95551,6 @@ async function addApp(appId, options, throwErr = true) {
95471
95551
  f2.error(`Could not add app ${formatError(dbVersionError)}`);
95472
95552
  program.error("");
95473
95553
  }
95474
- await snag.track({
95475
- channel: "app",
95476
- event: "App Added",
95477
- icon: "\u{1F389}",
95478
- user_id: userId,
95479
- tags: {
95480
- "app-id": appId
95481
- },
95482
- notify: false
95483
- }).catch();
95484
95554
  f2.success(`App ${appId} added to Capgo. ${throwErr ? "You can upload a bundle now" : ""}`);
95485
95555
  if (throwErr) {
95486
95556
  $e(`Done \u2705`);
@@ -95498,23 +95568,23 @@ var codeInject = "CapacitorUpdater.notifyAppReady()";
95498
95568
  var regexImport = /import.*from.*/g;
95499
95569
  var defaultChannel = "production";
95500
95570
  var execOption = { stdio: "pipe" };
95501
- async function cancelCommand2(command, userId, snag) {
95571
+ async function cancelCommand2(command, orgId, snag) {
95502
95572
  if (eD(command)) {
95503
- await markSnag("onboarding-v2", userId, snag, "canceled", "\u{1F937}");
95573
+ await markSnag("onboarding-v2", orgId, snag, "canceled", "\u{1F937}");
95504
95574
  import_node_process16.default.exit();
95505
95575
  }
95506
95576
  }
95507
- async function markStep(userId, snag, step) {
95508
- return markSnag("onboarding-v2", userId, snag, `onboarding-step-${step}`);
95577
+ async function markStep(orgId, snag, step) {
95578
+ return markSnag("onboarding-v2", orgId, snag, `onboarding-step-${step}`);
95509
95579
  }
95510
- async function step2(userId, snag, appId, options) {
95580
+ async function step2(organization, snag, appId, options) {
95511
95581
  const pm2 = getPMAndCommand();
95512
95582
  const doAdd = await se({ message: `Add ${appId} in Capgo?` });
95513
- await cancelCommand2(doAdd, userId, snag);
95583
+ await cancelCommand2(doAdd, organization.gid, snag);
95514
95584
  if (doAdd) {
95515
95585
  const s = de();
95516
95586
  s.start(`Running: ${pm2.runner} @capgo/cli@latest app add ${appId}`);
95517
- const addRes = await addApp(appId, options, false);
95587
+ const addRes = await addAppInternal(appId, options, organization, false);
95518
95588
  if (!addRes)
95519
95589
  s.stop(`App already add \u2705`);
95520
95590
  else
@@ -95522,12 +95592,12 @@ async function step2(userId, snag, appId, options) {
95522
95592
  } else {
95523
95593
  f2.info(`Run yourself "${pm2.runner} @capgo/cli@latest app add ${appId}"`);
95524
95594
  }
95525
- await markStep(userId, snag, 2);
95595
+ await markStep(organization.gid, snag, 2);
95526
95596
  }
95527
- async function step3(userId, snag, apikey, appId) {
95597
+ async function step3(orgId, snag, apikey, appId) {
95528
95598
  const pm2 = getPMAndCommand();
95529
95599
  const doChannel = await se({ message: `Create default channel ${defaultChannel} for ${appId} in Capgo?` });
95530
- await cancelCommand2(doChannel, userId, snag);
95600
+ await cancelCommand2(doChannel, orgId, snag);
95531
95601
  if (doChannel) {
95532
95602
  const s = de();
95533
95603
  s.start(`Running: ${pm2.runner} @capgo/cli@latest channel add ${defaultChannel} ${appId} --default`);
@@ -95542,14 +95612,14 @@ async function step3(userId, snag, apikey, appId) {
95542
95612
  } else {
95543
95613
  f2.info(`Run yourself "${pm2.runner} @capgo/cli@latest channel add ${defaultChannel} ${appId} --default"`);
95544
95614
  }
95545
- await markStep(userId, snag, 3);
95615
+ await markStep(orgId, snag, 3);
95546
95616
  }
95547
95617
  var urlMigrateV6 = "https://capacitorjs.com/docs/updating/6-0";
95548
95618
  var urlMigrateV5 = "https://capacitorjs.com/docs/updating/5-0";
95549
- async function step4(userId, snag, apikey, appId) {
95619
+ async function step4(orgId, snag, apikey, appId) {
95550
95620
  const pm2 = getPMAndCommand();
95551
95621
  const doInstall = await se({ message: `Automatic Install "@capgo/capacitor-updater" dependency in ${appId}?` });
95552
- await cancelCommand2(doInstall, userId, snag);
95622
+ await cancelCommand2(doInstall, orgId, snag);
95553
95623
  if (doInstall) {
95554
95624
  const s = de();
95555
95625
  s.start(`Checking if @capgo/capacitor-updater is installed`);
@@ -95586,11 +95656,11 @@ async function step4(userId, snag, apikey, appId) {
95586
95656
  } else {
95587
95657
  f2.info(`Run yourself "${pm2.installCommand} @capgo/capacitor-updater@latest"`);
95588
95658
  }
95589
- await markStep(userId, snag, 4);
95659
+ await markStep(orgId, snag, 4);
95590
95660
  }
95591
- async function step5(userId, snag, apikey, appId) {
95661
+ async function step5(orgId, snag, apikey, appId) {
95592
95662
  const doAddCode = await se({ message: `Automatic Add "${codeInject}" code and import in ${appId}?` });
95593
- await cancelCommand2(doAddCode, userId, snag);
95663
+ await cancelCommand2(doAddCode, orgId, snag);
95594
95664
  if (doAddCode) {
95595
95665
  const s = de();
95596
95666
  s.start(`Adding @capacitor-updater to your main file`);
@@ -95627,7 +95697,7 @@ ${codeInject};
95627
95697
  (0, import_node_fs11.writeFileSync)(mainFilePath, newMainFileContent);
95628
95698
  s.stop(`Code added to ${mainFilePath} \u2705`);
95629
95699
  }
95630
- await markStep(userId, snag, 5);
95700
+ await markStep(orgId, snag, 5);
95631
95701
  } else {
95632
95702
  f2.info(`Add to your main file the following code:
95633
95703
 
@@ -95637,10 +95707,10 @@ ${codeInject};
95637
95707
  `);
95638
95708
  }
95639
95709
  }
95640
- async function step6(userId, snag, apikey, appId) {
95710
+ async function step6(orgId, snag, apikey, appId) {
95641
95711
  const pm2 = getPMAndCommand();
95642
95712
  const doEncrypt = await se({ message: `Automatic configure end-to-end encryption in ${appId} updates?` });
95643
- await cancelCommand2(doEncrypt, userId, snag);
95713
+ await cancelCommand2(doEncrypt, orgId, snag);
95644
95714
  if (doEncrypt) {
95645
95715
  const s = de();
95646
95716
  s.start(`Running: ${pm2.runner} @capgo/cli@latest key create`);
@@ -95653,14 +95723,14 @@ async function step6(userId, snag, apikey, appId) {
95653
95723
  } else {
95654
95724
  s.stop(`key created \u{1F511}`);
95655
95725
  }
95656
- markSnag("onboarding-v2", userId, snag, "Use encryption");
95726
+ markSnag("onboarding-v2", orgId, snag, "Use encryption");
95657
95727
  }
95658
- await markStep(userId, snag, 6);
95728
+ await markStep(orgId, snag, 6);
95659
95729
  }
95660
- async function step7(userId, snag, apikey, appId) {
95730
+ async function step7(orgId, snag, apikey, appId) {
95661
95731
  const pm2 = getPMAndCommand();
95662
95732
  const doBuild = await se({ message: `Automatic build ${appId} with "${pm2.pm} run build" ?` });
95663
- await cancelCommand2(doBuild, userId, snag);
95733
+ await cancelCommand2(doBuild, orgId, snag);
95664
95734
  if (doBuild) {
95665
95735
  const s = de();
95666
95736
  const projectType = await findProjectType();
@@ -95678,12 +95748,12 @@ async function step7(userId, snag, apikey, appId) {
95678
95748
  } else {
95679
95749
  f2.info(`Build yourself with command: ${pm2.pm} run build && ${pm2.runner} cap sync`);
95680
95750
  }
95681
- await markStep(userId, snag, 7);
95751
+ await markStep(orgId, snag, 7);
95682
95752
  }
95683
- async function step8(userId, snag, apikey, appId) {
95753
+ async function step8(orgId, snag, apikey, appId) {
95684
95754
  const pm2 = getPMAndCommand();
95685
95755
  const doBundle = await se({ message: `Automatic upload ${appId} bundle to Capgo?` });
95686
- await cancelCommand2(doBundle, userId, snag);
95756
+ await cancelCommand2(doBundle, orgId, snag);
95687
95757
  if (doBundle) {
95688
95758
  const s = de();
95689
95759
  s.start(`Running: ${pm2.runner} @capgo/cli@latest bundle upload`);
@@ -95702,12 +95772,12 @@ async function step8(userId, snag, apikey, appId) {
95702
95772
  } else {
95703
95773
  f2.info(`Upload yourself with command: ${pm2.runner} @capgo/cli@latest bundle upload`);
95704
95774
  }
95705
- await markStep(userId, snag, 8);
95775
+ await markStep(orgId, snag, 8);
95706
95776
  }
95707
- async function step9(userId, snag) {
95777
+ async function step9(orgId, snag) {
95708
95778
  const pm2 = getPMAndCommand();
95709
95779
  const doRun = await se({ message: `Run in device now ?` });
95710
- await cancelCommand2(doRun, userId, snag);
95780
+ await cancelCommand2(doRun, orgId, snag);
95711
95781
  if (doRun) {
95712
95782
  const plaformType = await ie({
95713
95783
  message: "Pick a platform to run your app",
@@ -95728,20 +95798,7 @@ async function step9(userId, snag) {
95728
95798
  } else {
95729
95799
  f2.info(`Run yourself with command: ${pm2.runner} cap run <ios|android>`);
95730
95800
  }
95731
- await markStep(userId, snag, 9);
95732
- }
95733
- async function step10(userId, snag, supabase, appId) {
95734
- const doRun = await se({ message: `Automatic check if update working in device ?` });
95735
- await cancelCommand2(doRun, userId, snag);
95736
- if (doRun) {
95737
- f2.info(`Wait logs sent to Capgo from ${appId} device, Put the app in background and open it again.`);
95738
- f2.info("Waiting...");
95739
- await waitLog("onboarding-v2", supabase, appId, snag, userId);
95740
- } else {
95741
- const appIdUrl = convertAppName(appId);
95742
- f2.info(`Check logs in https://web.capgo.app/app/p/${appIdUrl}/logs to see if update works.`);
95743
- }
95744
- await markStep(userId, snag, 10);
95801
+ await markStep(orgId, snag, 9);
95745
95802
  }
95746
95803
  async function initApp(apikeyCommand, appId, options) {
95747
95804
  const pm2 = getPMAndCommand();
@@ -95758,18 +95815,19 @@ async function initApp(apikeyCommand, appId, options) {
95758
95815
  log.stop("Login Done \u2705");
95759
95816
  }
95760
95817
  const supabase = await createSupabaseClient(apikey);
95761
- const userId = await verifyUser(supabase, apikey, ["upload", "all", "read", "write"]);
95762
- await markStep(userId, snag, 1);
95763
- await step2(userId, snag, appId, options);
95764
- await step3(userId, snag, apikey, appId);
95765
- await step4(userId, snag, apikey, appId);
95766
- await step5(userId, snag, apikey, appId);
95767
- await step6(userId, snag, apikey, appId);
95768
- await step7(userId, snag, apikey, appId);
95769
- await step8(userId, snag, apikey, appId);
95770
- await step9(userId, snag);
95771
- await step10(userId, snag, supabase, appId);
95772
- await markStep(userId, snag, 0);
95818
+ await verifyUser(supabase, apikey, ["upload", "all", "read", "write"]);
95819
+ const organization = await getOrganization(supabase, ["admin", "super_admin"]);
95820
+ const orgId = organization.gid;
95821
+ await markStep(orgId, snag, 1);
95822
+ await step2(organization, snag, appId, options);
95823
+ await step3(orgId, snag, apikey, appId);
95824
+ await step4(orgId, snag, apikey, appId);
95825
+ await step5(orgId, snag, apikey, appId);
95826
+ await step6(orgId, snag, apikey, appId);
95827
+ await step7(orgId, snag, apikey, appId);
95828
+ await step8(orgId, snag, apikey, appId);
95829
+ await step9(orgId, snag);
95830
+ await markStep(orgId, snag, 0);
95773
95831
  f2.info(`Welcome onboard \u2708\uFE0F!`);
95774
95832
  f2.info(`Your Capgo update system is setup`);
95775
95833
  f2.info(`Next time use \`${pm2.runner} @capgo/cli@latest bundle upload\` to only upload your bundle`);
@@ -96641,7 +96699,7 @@ var bundle = program.command("bundle").description("Manage bundle");
96641
96699
  bundle.command("upload [appId]").alias("u").description("Upload a new bundle in Capgo Cloud").action(uploadCommand).option("-a, --apikey <apikey>", "apikey to link to your account").option("-p, --path <path>", "path of the folder to upload").option("-c, --channel <channel>", "channel to link to").option("-e, --external <url>", "link to external url intead of upload to Capgo Cloud").option("--iv-session-key <key>", "Set the iv and session key for bundle url external").option("--s3-region <region>", "Region for your AWS S3 bucket").option("--s3-apikey <apikey>", "apikey for your AWS S3 account").option("--s3-apisecret <apisecret>", "api secret for your AWS S3 account").option("--s3-bucket-name <bucketName>", "Name for your AWS S3 bucket").option("--key <key>", "custom path for public signing key").option("--key-data <keyData>", "base64 public signing key").option("--bundle-url", "prints bundle url into stdout").option("--no-key", "ignore signing key and send clear update").option("--no-code-check", "Ignore checking if notifyAppReady() is called in soure code and index present in root folder").option("--display-iv-session", "Show in the console the iv and session key used to encrypt the update").option("-b, --bundle <bundle>", "bundle version number of the bundle to upload").option(
96642
96700
  "--min-update-version <minUpdateVersion>",
96643
96701
  "Minimal version required to update to this version. Used only if the disable auto update is set to metadata in channel"
96644
- ).option("--auto-min-update-version", "Set the min update version based on native packages").option("--ignore-metadata-check", "Ignores the metadata (node_modules) check when uploading").option("--timeout <timeout>", "Timeout for the upload process in seconds");
96702
+ ).option("--auto-min-update-version", "Set the min update version based on native packages").option("--ignore-metadata-check", "Ignores the metadata (node_modules) check when uploading").option("--timeout <timeout>", "Timeout for the upload process in seconds").option("--multipart", "Uses multipart protocol to upload data to S3");
96645
96703
  bundle.command("compatibility [appId]").action(checkCompatibilityCommand).option("-a, --apikey <apikey>", "apikey to link to your account").option("-c, --channel <channel>", "channel to check the compatibility with").option("--text", "output text instead of emojis");
96646
96704
  bundle.command("delete [bundleId] [appId]").alias("d").description("Delete a bundle in Capgo Cloud").action(deleteBundle).option("-a, --apikey <apikey>", "apikey to link to your account");
96647
96705
  bundle.command("list [appId]").alias("l").description("List bundle in Capgo Cloud").action(listBundle).option("-a, --apikey <apikey>", "apikey to link to your account");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/cli",
3
- "version": "4.8.0",
3
+ "version": "4.8.2",
4
4
  "description": "A CLI to upload to capgo servers",
5
5
  "author": "github.com/riderx",
6
6
  "license": "Apache 2.0",
package/src/app/add.ts CHANGED
@@ -7,17 +7,24 @@ import * as p from '@clack/prompts'
7
7
  import { checkLatest } from '../api/update'
8
8
  import type { Options } from '../api/app'
9
9
  import { checkAppExists, newIconPath } from '../api/app'
10
+ import type {
11
+ Organization,
12
+ } from '../utils'
10
13
  import {
11
14
  checkPlanValid,
12
15
  createSupabaseClient,
13
16
  findSavedKey,
14
17
  formatError,
15
18
  getConfig,
16
- useLogSnag,
19
+ getOrganization,
17
20
  verifyUser,
18
21
  } from '../utils'
19
22
 
20
23
  export async function addApp(appId: string, options: Options, throwErr = true) {
24
+ await addAppInternal(appId, options, undefined, throwErr)
25
+ }
26
+
27
+ export async function addAppInternal(appId: string, options: Options, organization?: Organization, throwErr = true) {
21
28
  if (throwErr)
22
29
  p.intro(`Adding`)
23
30
 
@@ -25,7 +32,6 @@ export async function addApp(appId: string, options: Options, throwErr = true) {
25
32
  options.apikey = options.apikey || findSavedKey()
26
33
  const config = await getConfig()
27
34
  appId = appId || config?.app?.appId
28
- const snag = useLogSnag()
29
35
 
30
36
  if (!options.apikey) {
31
37
  p.log.error(`Missing API key, you need to provide a API key to upload your bundle`)
@@ -43,7 +49,7 @@ export async function addApp(appId: string, options: Options, throwErr = true) {
43
49
 
44
50
  const supabase = await createSupabaseClient(options.apikey)
45
51
 
46
- let userId = await verifyUser(supabase, options.apikey, ['write', 'all'])
52
+ const userId = await verifyUser(supabase, options.apikey, ['write', 'all'])
47
53
 
48
54
  // Check we have app access to this appId
49
55
  const appExist = await checkAppExists(supabase, appId)
@@ -55,36 +61,10 @@ export async function addApp(appId: string, options: Options, throwErr = true) {
55
61
  return false
56
62
  }
57
63
 
58
- const { error: orgError, data: allOrganizations } = await supabase
59
- .rpc('get_orgs_v5')
60
-
61
- if (orgError) {
62
- p.log.error('Cannot get the list of organizations - exiting')
63
- p.log.error(`Error ${JSON.stringify(orgError)}`)
64
- program.error('')
65
- }
66
-
67
- const adminOrgs = allOrganizations.filter(org => org.role === 'admin' || org.role === 'super_admin')
68
-
69
- const organizationUidRaw = (adminOrgs.length > 1)
70
- ? await p.select({
71
- message: 'Please pick the organization that you want to insert to',
72
- options: adminOrgs.map((org) => {
73
- return { value: org.gid, label: org.name }
74
- }),
75
- })
76
- : adminOrgs[0].gid
77
-
78
- if (p.isCancel(organizationUidRaw)) {
79
- p.log.error('Canceled organization selection, exiting')
80
- program.error('')
81
- }
82
-
83
- const organizationUid = organizationUidRaw as string
84
- const organization = allOrganizations.find(org => org.gid === organizationUid)!
85
- userId = organization.created_by
64
+ if (!organization)
65
+ organization = await getOrganization(supabase, ['admin', 'super_admin'])
86
66
 
87
- p.log.info(`Using the organization "${organization.name}" as the app owner`)
67
+ const organizationUid = organization.gid
88
68
 
89
69
  await checkPlanValid(supabase, organizationUid, options.apikey, undefined, false)
90
70
 
@@ -168,16 +148,6 @@ export async function addApp(appId: string, options: Options, throwErr = true) {
168
148
  p.log.error(`Could not add app ${formatError(dbVersionError)}`)
169
149
  program.error('')
170
150
  }
171
- await snag.track({
172
- channel: 'app',
173
- event: 'App Added',
174
- icon: '🎉',
175
- user_id: userId,
176
- tags: {
177
- 'app-id': appId,
178
- },
179
- notify: false,
180
- }).catch()
181
151
  p.log.success(`App ${appId} added to Capgo. ${throwErr ? 'You can upload a bundle now' : ''}`)
182
152
  if (throwErr) {
183
153
  p.outro(`Done ✅`)
package/src/app/debug.ts CHANGED
@@ -6,7 +6,7 @@ import type LogSnag from 'logsnag'
6
6
  import type { Database } from '../types/supabase.types'
7
7
  import { checkAppExistsAndHasPermissionOrgErr } from '../api/app'
8
8
  import { checkLatest } from '../api/update'
9
- import { OrganizationPerm, convertAppName, createSupabaseClient, findSavedKey, formatError, getConfig, getLocalConfig, useLogSnag, verifyUser } from '../utils'
9
+ import { OrganizationPerm, convertAppName, createSupabaseClient, findSavedKey, formatError, getConfig, getLocalConfig, getOrganizationId, useLogSnag, verifyUser } from '../utils'
10
10
 
11
11
  function wait(ms: number) {
12
12
  return new Promise((resolve) => {
@@ -19,19 +19,19 @@ export interface OptionsBaseDebug {
19
19
  device?: string
20
20
  }
21
21
 
22
- export async function markSnag(channel: string, userId: string, snag: LogSnag, event: string, icon = '✅') {
22
+ export async function markSnag(channel: string, orgId: string, snag: LogSnag, event: string, icon = '✅') {
23
23
  await snag.track({
24
24
  channel,
25
25
  event,
26
26
  icon,
27
- user_id: userId,
27
+ user_id: orgId,
28
28
  notify: false,
29
29
  }).catch()
30
30
  }
31
31
 
32
- export async function cancelCommand(channel: string, command: boolean | symbol, userId: string, snag: LogSnag) {
32
+ export async function cancelCommand(channel: string, command: boolean | symbol, orgId: string, snag: LogSnag) {
33
33
  if (p.isCancel(command)) {
34
- await markSnag(channel, userId, snag, 'canceled', '🤷')
34
+ await markSnag(channel, orgId, snag, 'canceled', '🤷')
35
35
  process.exit()
36
36
  }
37
37
  }
@@ -65,13 +65,13 @@ export async function getStats(supabase: SupabaseClient<Database>, query: QueryS
65
65
  return null
66
66
  }
67
67
 
68
- export async function waitLog(channel: string, supabase: SupabaseClient<Database>, appId: string, snag: LogSnag, userId: string, deviceId?: string) {
68
+ export async function waitLog(channel: string, supabase: SupabaseClient<Database>, appId: string, snag: LogSnag, orgId: string, deviceId?: string) {
69
69
  let loop = true
70
70
  let now = new Date().toISOString()
71
71
  const appIdUrl = convertAppName(appId)
72
72
  const config = await getLocalConfig()
73
73
  const baseUrl = `${config.hostWeb}/app/p/${appIdUrl}`
74
- await markSnag(channel, userId, snag, 'Use waitlog')
74
+ await markSnag(channel, orgId, snag, 'Use waitlog')
75
75
  const query: QueryStats = {
76
76
  appId,
77
77
  devicesId: deviceId ? [deviceId] : undefined,
@@ -90,13 +90,13 @@ export async function waitLog(channel: string, supabase: SupabaseClient<Database
90
90
  p.log.info(`Log from Device: ${data.device_id}`)
91
91
  if (data.action === 'get') {
92
92
  p.log.info('Update Sent your your device, wait until event download complete')
93
- await markSnag(channel, userId, snag, 'done')
93
+ await markSnag(channel, orgId, snag, 'done')
94
94
  }
95
95
  else if (data.action.startsWith('download_')) {
96
96
  const action = data.action.split('_')[1]
97
97
  if (action === 'complete') {
98
98
  p.log.info('Your bundle has been downloaded on your device, background the app now and open it again to see the update')
99
- await markSnag(channel, userId, snag, 'downloaded')
99
+ await markSnag(channel, orgId, snag, 'downloaded')
100
100
  }
101
101
  else if (action === 'fail') {
102
102
  p.log.error('Your bundle has failed to download on your device.')
@@ -109,7 +109,7 @@ export async function waitLog(channel: string, supabase: SupabaseClient<Database
109
109
  else if (data.action === 'set') {
110
110
  p.log.info('Your bundle has been set on your device ❤️')
111
111
  loop = false
112
- await markSnag(channel, userId, snag, 'set')
112
+ await markSnag(channel, orgId, snag, 'set')
113
113
  return Promise.resolve(data)
114
114
  }
115
115
  else if (data.action === 'NoChannelOrOverride') {
@@ -202,12 +202,14 @@ export async function debugApp(appId: string, options: OptionsBaseDebug) {
202
202
  // Check we have app access to this appId
203
203
  await checkAppExistsAndHasPermissionOrgErr(supabase, options.apikey, appId, OrganizationPerm.admin)
204
204
 
205
+ const orgId = await getOrganizationId(supabase, appId)
206
+
205
207
  const doRun = await p.confirm({ message: `Automatic check if update working in device ?` })
206
208
  await cancelCommand('debug', doRun, userId, snag)
207
209
  if (doRun) {
208
210
  p.log.info(`Wait logs sent to Capgo from ${appId} device, Put the app in background and open it again.`)
209
211
  p.log.info('Waiting...')
210
- await waitLog('debug', supabase, appId, snag, userId, deviceId)
212
+ await waitLog('debug', supabase, appId, snag, orgId, deviceId)
211
213
  p.outro(`Done ✅`)
212
214
  }
213
215
  else {
@@ -39,6 +39,7 @@ import {
39
39
  requireUpdateMetadata,
40
40
  updateOrCreateChannel,
41
41
  updateOrCreateVersion,
42
+ uploadMultipart,
42
43
  uploadUrl,
43
44
  useLogSnag,
44
45
  verifyUser,
@@ -66,6 +67,7 @@ interface Options extends OptionsBase {
66
67
  autoMinUpdateVersion?: boolean
67
68
  ignoreMetadataCheck?: boolean
68
69
  timeout?: number
70
+ multipart?: boolean
69
71
  }
70
72
 
71
73
  const UPLOAD_TIMEOUT = 120000
@@ -381,26 +383,32 @@ It will be also visible in your dashboard\n`)
381
383
  if (!external && zipped) {
382
384
  const spinner = p.spinner()
383
385
  spinner.start(`Uploading Bundle`)
384
-
385
- const url = await uploadUrl(supabase, appid, bundle)
386
- if (!url) {
387
- p.log.error(`Cannot get upload url`)
388
- program.error('')
389
- }
390
386
  const startTime = performance.now()
387
+
391
388
  try {
392
- await ky.put(url, {
393
- timeout: options.timeout || UPLOAD_TIMEOUT,
394
- retry: 5,
395
- body: zipped,
396
- headers: (!localS3
397
- ? {
398
- 'Content-Type': 'application/octet-stream',
399
- 'Cache-Control': 'public, max-age=456789, immutable',
400
- 'x-amz-meta-crc32': checksum,
401
- }
402
- : undefined),
403
- })
389
+ if (options.multipart !== undefined && options.multipart) {
390
+ await uploadMultipart(supabase, appid, bundle, zipped)
391
+ }
392
+ else {
393
+ const url = await uploadUrl(supabase, appid, bundle)
394
+ if (!url) {
395
+ p.log.error(`Cannot get upload url`)
396
+ program.error('')
397
+ }
398
+
399
+ await ky.put(url, {
400
+ timeout: options.timeout || UPLOAD_TIMEOUT,
401
+ retry: 5,
402
+ body: zipped,
403
+ headers: (!localS3
404
+ ? {
405
+ 'Content-Type': 'application/octet-stream',
406
+ 'Cache-Control': 'public, max-age=456789, immutable',
407
+ 'x-amz-meta-crc32': checksum,
408
+ }
409
+ : undefined),
410
+ })
411
+ }
404
412
  }
405
413
  catch (errorUpload) {
406
414
  const endTime = performance.now()
@@ -411,6 +419,7 @@ It will be also visible in your dashboard\n`)
411
419
  await deletedFailedVersion(supabase, appid, bundle)
412
420
  program.error('')
413
421
  }
422
+
414
423
  versionData.storage_provider = 'r2'
415
424
  const { error: dbError2 } = await updateOrCreateVersion(supabase, versionData)
416
425
  if (dbError2) {
package/src/index.ts CHANGED
@@ -131,6 +131,7 @@ bundle
131
131
  .option('--auto-min-update-version', 'Set the min update version based on native packages')
132
132
  .option('--ignore-metadata-check', 'Ignores the metadata (node_modules) check when uploading')
133
133
  .option('--timeout <timeout>', 'Timeout for the upload process in seconds')
134
+ .option('--multipart', 'Uses multipart protocol to upload data to S3')
134
135
 
135
136
  bundle
136
137
  .command('compatibility [appId]')
package/src/init.ts CHANGED
@@ -12,10 +12,11 @@ import { createKey } from './key'
12
12
  import { addChannel } from './channel/add'
13
13
  import { uploadBundle } from './bundle/upload'
14
14
  import { doLoginExists, login } from './login'
15
- import { addApp } from './app/add'
15
+ import { addAppInternal } from './app/add'
16
16
  import { checkLatest } from './api/update'
17
17
  import type { Options } from './api/app'
18
- import { convertAppName, createSupabaseClient, findBuildCommandForProjectType, findMainFile, findMainFileForProjectType, findProjectType, findSavedKey, getConfig, getPMAndCommand, useLogSnag, verifyUser } from './utils'
18
+ import type { Organization } from './utils'
19
+ import { convertAppName, createSupabaseClient, findBuildCommandForProjectType, findMainFile, findMainFileForProjectType, findProjectType, findSavedKey, getConfig, getOrganization, getPMAndCommand, useLogSnag, verifyUser } from './utils'
19
20
 
20
21
  interface SuperOptions extends Options {
21
22
  local: boolean
@@ -27,25 +28,25 @@ const regexImport = /import.*from.*/g
27
28
  const defaultChannel = 'production'
28
29
  const execOption = { stdio: 'pipe' }
29
30
 
30
- async function cancelCommand(command: boolean | symbol, userId: string, snag: LogSnag) {
31
+ async function cancelCommand(command: boolean | symbol, orgId: string, snag: LogSnag) {
31
32
  if (p.isCancel(command)) {
32
- await markSnag('onboarding-v2', userId, snag, 'canceled', '🤷')
33
+ await markSnag('onboarding-v2', orgId, snag, 'canceled', '🤷')
33
34
  process.exit()
34
35
  }
35
36
  }
36
37
 
37
- async function markStep(userId: string, snag: LogSnag, step: number | string) {
38
- return markSnag('onboarding-v2', userId, snag, `onboarding-step-${step}`)
38
+ async function markStep(orgId: string, snag: LogSnag, step: number | string) {
39
+ return markSnag('onboarding-v2', orgId, snag, `onboarding-step-${step}`)
39
40
  }
40
41
 
41
- async function step2(userId: string, snag: LogSnag, appId: string, options: SuperOptions) {
42
+ async function step2(organization: Organization, snag: LogSnag, appId: string, options: SuperOptions) {
42
43
  const pm = getPMAndCommand()
43
44
  const doAdd = await p.confirm({ message: `Add ${appId} in Capgo?` })
44
- await cancelCommand(doAdd, userId, snag)
45
+ await cancelCommand(doAdd, organization.gid, snag)
45
46
  if (doAdd) {
46
47
  const s = p.spinner()
47
48
  s.start(`Running: ${pm.runner} @capgo/cli@latest app add ${appId}`)
48
- const addRes = await addApp(appId, options, false)
49
+ const addRes = await addAppInternal(appId, options, organization, false)
49
50
  if (!addRes)
50
51
  s.stop(`App already add ✅`)
51
52
  else
@@ -54,13 +55,13 @@ async function step2(userId: string, snag: LogSnag, appId: string, options: Supe
54
55
  else {
55
56
  p.log.info(`Run yourself "${pm.runner} @capgo/cli@latest app add ${appId}"`)
56
57
  }
57
- await markStep(userId, snag, 2)
58
+ await markStep(organization.gid, snag, 2)
58
59
  }
59
60
 
60
- async function step3(userId: string, snag: LogSnag, apikey: string, appId: string) {
61
+ async function step3(orgId: string, snag: LogSnag, apikey: string, appId: string) {
61
62
  const pm = getPMAndCommand()
62
63
  const doChannel = await p.confirm({ message: `Create default channel ${defaultChannel} for ${appId} in Capgo?` })
63
- await cancelCommand(doChannel, userId, snag)
64
+ await cancelCommand(doChannel, orgId, snag)
64
65
  if (doChannel) {
65
66
  const s = p.spinner()
66
67
  // create production channel public
@@ -77,15 +78,15 @@ async function step3(userId: string, snag: LogSnag, apikey: string, appId: strin
77
78
  else {
78
79
  p.log.info(`Run yourself "${pm.runner} @capgo/cli@latest channel add ${defaultChannel} ${appId} --default"`)
79
80
  }
80
- await markStep(userId, snag, 3)
81
+ await markStep(orgId, snag, 3)
81
82
  }
82
83
 
83
84
  const urlMigrateV6 = 'https://capacitorjs.com/docs/updating/6-0'
84
85
  const urlMigrateV5 = 'https://capacitorjs.com/docs/updating/5-0'
85
- async function step4(userId: string, snag: LogSnag, apikey: string, appId: string) {
86
+ async function step4(orgId: string, snag: LogSnag, apikey: string, appId: string) {
86
87
  const pm = getPMAndCommand()
87
88
  const doInstall = await p.confirm({ message: `Automatic Install "@capgo/capacitor-updater" dependency in ${appId}?` })
88
- await cancelCommand(doInstall, userId, snag)
89
+ await cancelCommand(doInstall, orgId, snag)
89
90
  if (doInstall) {
90
91
  const s = p.spinner()
91
92
  s.start(`Checking if @capgo/capacitor-updater is installed`)
@@ -129,12 +130,12 @@ async function step4(userId: string, snag: LogSnag, apikey: string, appId: strin
129
130
  else {
130
131
  p.log.info(`Run yourself "${pm.installCommand} @capgo/capacitor-updater@latest"`)
131
132
  }
132
- await markStep(userId, snag, 4)
133
+ await markStep(orgId, snag, 4)
133
134
  }
134
135
 
135
- async function step5(userId: string, snag: LogSnag, apikey: string, appId: string) {
136
+ async function step5(orgId: string, snag: LogSnag, apikey: string, appId: string) {
136
137
  const doAddCode = await p.confirm({ message: `Automatic Add "${codeInject}" code and import in ${appId}?` })
137
- await cancelCommand(doAddCode, userId, snag)
138
+ await cancelCommand(doAddCode, orgId, snag)
138
139
  if (doAddCode) {
139
140
  const s = p.spinner()
140
141
  s.start(`Adding @capacitor-updater to your main file`)
@@ -172,17 +173,17 @@ async function step5(userId: string, snag: LogSnag, apikey: string, appId: strin
172
173
  writeFileSync(mainFilePath, newMainFileContent)
173
174
  s.stop(`Code added to ${mainFilePath} ✅`)
174
175
  }
175
- await markStep(userId, snag, 5)
176
+ await markStep(orgId, snag, 5)
176
177
  }
177
178
  else {
178
179
  p.log.info(`Add to your main file the following code:\n\n${importInject};\n\n${codeInject};\n`)
179
180
  }
180
181
  }
181
182
 
182
- async function step6(userId: string, snag: LogSnag, apikey: string, appId: string) {
183
+ async function step6(orgId: string, snag: LogSnag, apikey: string, appId: string) {
183
184
  const pm = getPMAndCommand()
184
185
  const doEncrypt = await p.confirm({ message: `Automatic configure end-to-end encryption in ${appId} updates?` })
185
- await cancelCommand(doEncrypt, userId, snag)
186
+ await cancelCommand(doEncrypt, orgId, snag)
186
187
  if (doEncrypt) {
187
188
  const s = p.spinner()
188
189
  s.start(`Running: ${pm.runner} @capgo/cli@latest key create`)
@@ -196,15 +197,15 @@ async function step6(userId: string, snag: LogSnag, apikey: string, appId: strin
196
197
  else {
197
198
  s.stop(`key created 🔑`)
198
199
  }
199
- markSnag('onboarding-v2', userId, snag, 'Use encryption')
200
+ markSnag('onboarding-v2', orgId, snag, 'Use encryption')
200
201
  }
201
- await markStep(userId, snag, 6)
202
+ await markStep(orgId, snag, 6)
202
203
  }
203
204
 
204
- async function step7(userId: string, snag: LogSnag, apikey: string, appId: string) {
205
+ async function step7(orgId: string, snag: LogSnag, apikey: string, appId: string) {
205
206
  const pm = getPMAndCommand()
206
207
  const doBuild = await p.confirm({ message: `Automatic build ${appId} with "${pm.pm} run build" ?` })
207
- await cancelCommand(doBuild, userId, snag)
208
+ await cancelCommand(doBuild, orgId, snag)
208
209
  if (doBuild) {
209
210
  const s = p.spinner()
210
211
  const projectType = await findProjectType()
@@ -224,13 +225,13 @@ async function step7(userId: string, snag: LogSnag, apikey: string, appId: strin
224
225
  else {
225
226
  p.log.info(`Build yourself with command: ${pm.pm} run build && ${pm.runner} cap sync`)
226
227
  }
227
- await markStep(userId, snag, 7)
228
+ await markStep(orgId, snag, 7)
228
229
  }
229
230
 
230
- async function step8(userId: string, snag: LogSnag, apikey: string, appId: string) {
231
+ async function step8(orgId: string, snag: LogSnag, apikey: string, appId: string) {
231
232
  const pm = getPMAndCommand()
232
233
  const doBundle = await p.confirm({ message: `Automatic upload ${appId} bundle to Capgo?` })
233
- await cancelCommand(doBundle, userId, snag)
234
+ await cancelCommand(doBundle, orgId, snag)
234
235
  if (doBundle) {
235
236
  const s = p.spinner()
236
237
  s.start(`Running: ${pm.runner} @capgo/cli@latest bundle upload`)
@@ -251,13 +252,13 @@ async function step8(userId: string, snag: LogSnag, apikey: string, appId: strin
251
252
  else {
252
253
  p.log.info(`Upload yourself with command: ${pm.runner} @capgo/cli@latest bundle upload`)
253
254
  }
254
- await markStep(userId, snag, 8)
255
+ await markStep(orgId, snag, 8)
255
256
  }
256
257
 
257
- async function step9(userId: string, snag: LogSnag) {
258
+ async function step9(orgId: string, snag: LogSnag) {
258
259
  const pm = getPMAndCommand()
259
260
  const doRun = await p.confirm({ message: `Run in device now ?` })
260
- await cancelCommand(doRun, userId, snag)
261
+ await cancelCommand(doRun, orgId, snag)
261
262
  if (doRun) {
262
263
  const plaformType = await p.select({
263
264
  message: 'Pick a platform to run your app',
@@ -280,22 +281,22 @@ async function step9(userId: string, snag: LogSnag) {
280
281
  else {
281
282
  p.log.info(`Run yourself with command: ${pm.runner} cap run <ios|android>`)
282
283
  }
283
- await markStep(userId, snag, 9)
284
+ await markStep(orgId, snag, 9)
284
285
  }
285
286
 
286
- async function step10(userId: string, snag: LogSnag, supabase: SupabaseClient<Database>, appId: string) {
287
+ async function _step10(orgId: string, snag: LogSnag, supabase: SupabaseClient<Database>, appId: string) {
287
288
  const doRun = await p.confirm({ message: `Automatic check if update working in device ?` })
288
- await cancelCommand(doRun, userId, snag)
289
+ await cancelCommand(doRun, orgId, snag)
289
290
  if (doRun) {
290
291
  p.log.info(`Wait logs sent to Capgo from ${appId} device, Put the app in background and open it again.`)
291
292
  p.log.info('Waiting...')
292
- await waitLog('onboarding-v2', supabase, appId, snag, userId)
293
+ await waitLog('onboarding-v2', supabase, appId, snag, orgId)
293
294
  }
294
295
  else {
295
296
  const appIdUrl = convertAppName(appId)
296
297
  p.log.info(`Check logs in https://web.capgo.app/app/p/${appIdUrl}/logs to see if update works.`)
297
298
  }
298
- await markStep(userId, snag, 10)
299
+ await markStep(orgId, snag, 10)
299
300
  }
300
301
 
301
302
  export async function initApp(apikeyCommand: string, appId: string, options: SuperOptions) {
@@ -315,20 +316,24 @@ export async function initApp(apikeyCommand: string, appId: string, options: Sup
315
316
  }
316
317
 
317
318
  const supabase = await createSupabaseClient(apikey)
318
- const userId = await verifyUser(supabase, apikey, ['upload', 'all', 'read', 'write'])
319
- await markStep(userId, snag, 1)
319
+ await verifyUser(supabase, apikey, ['upload', 'all', 'read', 'write'])
320
320
 
321
- await step2(userId, snag, appId, options)
322
- await step3(userId, snag, apikey, appId)
323
- await step4(userId, snag, apikey, appId)
324
- await step5(userId, snag, apikey, appId)
325
- await step6(userId, snag, apikey, appId)
326
- await step7(userId, snag, apikey, appId)
327
- await step8(userId, snag, apikey, appId)
328
- await step9(userId, snag)
329
- await step10(userId, snag, supabase, appId)
321
+ const organization = await getOrganization(supabase, ['admin', 'super_admin'])
322
+ const orgId = organization.gid
330
323
 
331
- await markStep(userId, snag, 0)
324
+ await markStep(orgId, snag, 1)
325
+
326
+ await step2(organization, snag, appId, options)
327
+ await step3(orgId, snag, apikey, appId)
328
+ await step4(orgId, snag, apikey, appId)
329
+ await step5(orgId, snag, apikey, appId)
330
+ await step6(orgId, snag, apikey, appId)
331
+ await step7(orgId, snag, apikey, appId)
332
+ await step8(orgId, snag, apikey, appId)
333
+ await step9(orgId, snag)
334
+ // await step10(orgId, snag, supabase, appId)
335
+
336
+ await markStep(orgId, snag, 0)
332
337
  p.log.info(`Welcome onboard ✈️!`)
333
338
  p.log.info(`Your Capgo update system is setup`)
334
339
  p.log.info(`Next time use \`${pm.runner} @capgo/cli@latest bundle upload\` to only upload your bundle`)
package/src/utils.ts CHANGED
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
2
  import { homedir } from 'node:os'
3
3
  import { resolve } from 'node:path'
4
4
  import process from 'node:process'
5
+ import type { Buffer } from 'node:buffer'
5
6
  import { loadConfig } from '@capacitor/cli/dist/config'
6
7
  import { program } from 'commander'
7
8
  import type { SupabaseClient } from '@supabase/supabase-js'
@@ -21,6 +22,10 @@ export const defaultHost = 'https://capgo.app'
21
22
  export const defaultApiHost = 'https://api.capgo.app'
22
23
  export const defaultHostWeb = 'https://web.capgo.app'
23
24
 
25
+ export type ArrayElement<ArrayType extends readonly unknown[]> =
26
+ ArrayType extends readonly (infer ElementType)[] ? ElementType : never
27
+ export type Organization = ArrayElement<Database['public']['Functions']['get_orgs_v5']['Returns']>
28
+
24
29
  export const regexSemver = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
25
30
  export const formatError = (error: any) => error ? `\n${prettyjson.render(error)}` : ''
26
31
 
@@ -483,6 +488,7 @@ export async function uploadUrl(supabase: SupabaseClient<Database>, appId: strin
483
488
  const data = {
484
489
  app_id: appId,
485
490
  name,
491
+ version: 0,
486
492
  }
487
493
  try {
488
494
  const pathUploadLink = 'private/upload_link'
@@ -495,6 +501,67 @@ export async function uploadUrl(supabase: SupabaseClient<Database>, appId: strin
495
501
  return ''
496
502
  }
497
503
 
504
+ async function prepareMultipart(supabase: SupabaseClient<Database>, appId: string, name: string): Promise<{ key: string, uploadId: string, url: string } | null> {
505
+ const data = {
506
+ app_id: appId,
507
+ name,
508
+ version: 1,
509
+ }
510
+ try {
511
+ const pathUploadLink = 'private/upload_link'
512
+ const res = await supabase.functions.invoke(pathUploadLink, { body: JSON.stringify(data) })
513
+ return res.data as any
514
+ }
515
+ catch (error) {
516
+ p.log.error(`Cannot get upload url ${formatError(error)}`)
517
+ return null
518
+ }
519
+ }
520
+
521
+ async function finishMultipartDownload(key: string, uploadId: string, url: string, parts: any[]) {
522
+ const metadata = {
523
+ action: 'mpu-complete',
524
+ uploadId,
525
+ key,
526
+ }
527
+
528
+ await ky.post(url, {
529
+ json: {
530
+ parts,
531
+ },
532
+ searchParams: new URLSearchParams({ body: btoa(JSON.stringify(metadata)) }),
533
+ })
534
+
535
+ // console.log(await response.json())
536
+ }
537
+
538
+ const PART_SIZE = 10 * 1024 * 1024
539
+ export async function uploadMultipart(supabase: SupabaseClient<Database>, appId: string, name: string, data: Buffer): Promise<boolean> {
540
+ try {
541
+ const multipartPrep = await prepareMultipart(supabase, appId, name)
542
+ if (!multipartPrep) {
543
+ // Just pass the error
544
+ return false
545
+ }
546
+
547
+ const fileSize = data.length
548
+ const partCount = Math.ceil(fileSize / PART_SIZE)
549
+
550
+ const uploadPromises = Array.from({ length: partCount }, (_, index) =>
551
+ uploadPart(data, PART_SIZE, multipartPrep.url, multipartPrep.key, multipartPrep.uploadId, index))
552
+
553
+ const parts = await Promise.all(uploadPromises)
554
+
555
+ await finishMultipartDownload(multipartPrep.key, multipartPrep.uploadId, multipartPrep.url, parts)
556
+
557
+ return true
558
+ }
559
+ catch (e) {
560
+ p.log.error(`Could not upload via multipart ${formatError(e)}`)
561
+ return false
562
+ }
563
+ }
564
+
498
565
  export async function deletedFailedVersion(supabase: SupabaseClient<Database>, appId: string, name: string): Promise<void> {
499
566
  const data = {
500
567
  app_id: appId,
@@ -511,6 +578,34 @@ export async function deletedFailedVersion(supabase: SupabaseClient<Database>, a
511
578
  }
512
579
  }
513
580
 
581
+ async function uploadPart(
582
+ data: Buffer,
583
+ partsize: number,
584
+ url: string,
585
+ key: string,
586
+ uploadId: string,
587
+ index: number,
588
+ ) {
589
+ const dataToUpload = data.subarray(
590
+ partsize * index,
591
+ partsize * (index + 1),
592
+ )
593
+
594
+ const metadata = {
595
+ action: 'mpu-uploadpart',
596
+ uploadId,
597
+ partNumber: index + 1,
598
+ key,
599
+ }
600
+
601
+ const response = await ky.put(url, {
602
+ body: dataToUpload,
603
+ searchParams: new URLSearchParams({ body: btoa(JSON.stringify(metadata)) }),
604
+ })
605
+
606
+ return await response.json()
607
+ }
608
+
514
609
  export async function updateOrCreateChannel(supabase: SupabaseClient<Database>, update: Database['public']['Tables']['channels']['Insert']) {
515
610
  // console.log('updateOrCreateChannel', update)
516
611
  if (!update.app_id || !update.name || !update.created_by) {
@@ -562,12 +657,50 @@ export async function updateOrCreateChannel(supabase: SupabaseClient<Database>,
562
657
 
563
658
  export function useLogSnag(): LogSnag {
564
659
  const logsnag = new LogSnag({
565
- token: 'c124f5e9d0ce5bdd14bbb48f815d5583',
566
- project: 'capgo',
660
+ token: process.env.CAPGO_LOGSNAG ?? 'c124f5e9d0ce5bdd14bbb48f815d5583',
661
+ project: process.env.CAPGO_LOGSNAG_PROJECT ?? 'capgo',
567
662
  })
568
663
  return logsnag
569
664
  }
570
665
 
666
+ export async function getOrganization(supabase: SupabaseClient<Database>, roles: string[]): Promise<Organization> {
667
+ const { error: orgError, data: allOrganizations } = await supabase
668
+ .rpc('get_orgs_v5')
669
+
670
+ if (orgError) {
671
+ p.log.error('Cannot get the list of organizations - exiting')
672
+ p.log.error(`Error ${JSON.stringify(orgError)}`)
673
+ program.error('')
674
+ }
675
+
676
+ const adminOrgs = allOrganizations.filter(org => !!roles.find(role => role === org.role))
677
+
678
+ if (adminOrgs.length === 0) {
679
+ p.log.error(`Could not get organization with roles: ${roles.join(' or ')} because the user does not have any org`)
680
+ program.error('')
681
+ }
682
+
683
+ const organizationUidRaw = (adminOrgs.length > 1)
684
+ ? await p.select({
685
+ message: 'Please pick the organization that you want to insert to',
686
+ options: adminOrgs.map((org) => {
687
+ return { value: org.gid, label: org.name }
688
+ }),
689
+ })
690
+ : adminOrgs[0].gid
691
+
692
+ if (p.isCancel(organizationUidRaw)) {
693
+ p.log.error('Canceled organization selection, exiting')
694
+ program.error('')
695
+ }
696
+
697
+ const organizationUid = organizationUidRaw as string
698
+ const organization = allOrganizations.find(org => org.gid === organizationUid)!
699
+
700
+ p.log.info(`Using the organization "${organization.name}" as the app owner`)
701
+ return organization
702
+ }
703
+
571
704
  export const convertAppName = (appName: string) => appName.replace(/\./g, '--')
572
705
 
573
706
  export async function verifyUser(supabase: SupabaseClient<Database>, apikey: string, keymod: Database['public']['Enums']['key_mode'][] = ['all']) {