@appsemble/cli 0.27.10 → 0.27.12

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/lib/app.js CHANGED
@@ -20,12 +20,16 @@ import { publishResourcesRecursively } from './resource.js';
20
20
  * Traverses an app directory and appends the files it finds to the given FormData object.
21
21
  *
22
22
  * @param path The path of the app directory to traverse.
23
+ * @param context The Context to use from `.appsemblerc.yaml`.
23
24
  * @param formData The FormData object to append the results into..
24
25
  * @returns The context from `.appsemblerc.yaml` if a match was found.
25
26
  */
26
- export async function traverseAppDirectory(path, formData) {
27
+ export async function traverseAppDirectory(path, context, formData) {
27
28
  let rc;
29
+ let discoveredContext;
28
30
  let yaml;
31
+ let iconPath;
32
+ let maskableIconPath;
29
33
  let controllerPath;
30
34
  let controllerBuildConfig;
31
35
  let controllerBuildResult;
@@ -60,6 +64,10 @@ export async function traverseAppDirectory(path, formData) {
60
64
  formData.append('iconBackground', rc.iconBackground);
61
65
  gatheredData.iconBackground = rc.iconBackground;
62
66
  }
67
+ if (context && has(rc === null || rc === void 0 ? void 0 : rc.context, context)) {
68
+ discoveredContext = rc.context[context];
69
+ logger.verbose(`Using context: ${inspect(discoveredContext, { colors: true })}`);
70
+ }
63
71
  break;
64
72
  case 'app-definition.yaml': {
65
73
  logger.info(`Using app definition from ${filepath}`);
@@ -74,10 +82,12 @@ export async function traverseAppDirectory(path, formData) {
74
82
  }
75
83
  case 'icon.png':
76
84
  case 'icon.svg':
85
+ iconPath = filepath;
77
86
  gatheredData.iconUrl = filepath;
78
87
  return;
79
88
  case 'maskable-icon.png':
80
- return;
89
+ maskableIconPath = filepath;
90
+ break;
81
91
  case 'screenshots':
82
92
  return opendirSafe(filepath, (screenshotPath, screenshotStat) => {
83
93
  if (screenshotStat.isDirectory() &&
@@ -133,7 +143,14 @@ export async function traverseAppDirectory(path, formData) {
133
143
  if (yaml === undefined) {
134
144
  throw new AppsembleError('No app definition found');
135
145
  }
136
- return [rc, yaml, gatheredData];
146
+ discoveredContext || (discoveredContext = {});
147
+ discoveredContext.icon = discoveredContext.icon
148
+ ? resolve(path, discoveredContext.icon)
149
+ : iconPath;
150
+ discoveredContext.maskableIcon = discoveredContext.maskableIcon
151
+ ? resolve(path, discoveredContext.maskableIcon)
152
+ : maskableIconPath;
153
+ return [discoveredContext, rc, yaml, gatheredData];
137
154
  }
138
155
  async function retrieveContext(path, context) {
139
156
  let rc;
@@ -301,6 +318,182 @@ export async function publishSeedAssets(path, app, remote, assetsClonable) {
301
318
  logger.warn(`Missing assets directory in ${path}. Skipping...`);
302
319
  }
303
320
  }
321
+ function parseValueFromDefinition(value) {
322
+ let parsed = value;
323
+ if (typeof parsed === 'string' && parsed.includes('{{') && parsed.includes('}}')) {
324
+ parsed = parsed.replaceAll(/{{\s*([^\s{}]+)\s*}}/g, (match, variable) => process.env[variable.trim()] || match);
325
+ }
326
+ return parsed;
327
+ }
328
+ export function parseValues(type, name, valuesToParse) {
329
+ const parsedValues = {};
330
+ let missingValues = false;
331
+ for (const valueToParse of valuesToParse) {
332
+ const [key, value] = Object.entries(valueToParse)[0];
333
+ parsedValues[key] = parseValueFromDefinition(value);
334
+ if (!parsedValues[key]) {
335
+ logger.error(`Missing ${key} value for ${type} ${name}`);
336
+ missingValues = true;
337
+ }
338
+ }
339
+ return [parsedValues, missingValues];
340
+ }
341
+ export async function publishAppConfig(path, app, remote) {
342
+ const configPath = join(path, 'config');
343
+ logger.info(`Publishing app config from ${configPath}`);
344
+ if (existsSync(configPath)) {
345
+ try {
346
+ logger.info(`Deleting existing variables from app ${app.id}`);
347
+ await axios.delete(`/api/apps/${app.id}/variables`, { baseURL: remote });
348
+ const appVariablesPath = join(configPath, 'variables.json');
349
+ if (existsSync(appVariablesPath)) {
350
+ const [appVariables] = (await readData(appVariablesPath));
351
+ if (appVariables.length) {
352
+ logger.info(`Publishing ${appVariables.length} app variable(s)`);
353
+ for (const appVariable of appVariables) {
354
+ const { name, value } = appVariable;
355
+ const [parsedValues, missingValues] = parseValues('variable', name, [{ value }]);
356
+ if (missingValues) {
357
+ continue;
358
+ }
359
+ await axios.post(`/api/apps/${app.id}/variables`, { name, ...parsedValues }, { baseURL: remote });
360
+ }
361
+ }
362
+ }
363
+ }
364
+ catch (error) {
365
+ logger.error('Something went wrong when creating app variables:');
366
+ logger.error(error);
367
+ }
368
+ try {
369
+ logger.info(`Deleting existing service secrets from app ${app.id}`);
370
+ await axios.delete(`/api/apps/${app.id}/secrets/service`, { baseURL: remote });
371
+ const serviceSecretsPath = join(configPath, 'secrets', 'service.json');
372
+ if (existsSync(serviceSecretsPath)) {
373
+ const [serviceSecrets] = (await readData(serviceSecretsPath));
374
+ if (serviceSecrets.length) {
375
+ logger.info(`Publishing ${serviceSecrets.length} app service secret(s)`);
376
+ for (const serviceSecret of serviceSecrets) {
377
+ const { identifier, name, secret, urlPatterns, ...rest } = serviceSecret;
378
+ const [parsedValues, missingValues] = parseValues('service secret', name, [
379
+ { secret },
380
+ { identifier },
381
+ { urlPatterns },
382
+ ]);
383
+ if (missingValues) {
384
+ continue;
385
+ }
386
+ await axios.post(`/api/apps/${app.id}/secrets/service`, { name, ...parsedValues, ...rest }, { baseURL: remote });
387
+ }
388
+ }
389
+ }
390
+ }
391
+ catch (error) {
392
+ logger.error('Something went wrong when creating app service secrets:');
393
+ logger.error(error);
394
+ }
395
+ try {
396
+ logger.info(`Deleting existing saml secrets from app ${app.id}`);
397
+ await axios.delete(`/api/apps/${app.id}/secrets/saml`, { baseURL: remote });
398
+ const samlSecretsPath = join(configPath, 'secrets', 'saml.json');
399
+ if (existsSync(samlSecretsPath)) {
400
+ const [samlSecrets] = (await readData(samlSecretsPath));
401
+ if (samlSecrets.length) {
402
+ logger.info(`Publishing ${samlSecrets.length} app saml secret(s)`);
403
+ for (const samlSecret of samlSecrets) {
404
+ const { entityId, idpCertificate, name, ssoUrl, ...rest } = samlSecret;
405
+ const [parsedValues, missingValues] = parseValues('saml secret', name, [
406
+ { entityId },
407
+ { idpCertificate },
408
+ { ssoUrl },
409
+ ]);
410
+ if (missingValues) {
411
+ continue;
412
+ }
413
+ await axios.post(`/api/apps/${app.id}/secrets/saml`, { name, ...parsedValues, ...rest }, { baseURL: remote });
414
+ }
415
+ }
416
+ }
417
+ }
418
+ catch (error) {
419
+ logger.error('Something went wrong when creating app saml secrets:');
420
+ logger.error(error);
421
+ }
422
+ try {
423
+ logger.info(`Deleting existing oauth2 secrets from app ${app.id}`);
424
+ await axios.delete(`/api/apps/${app.id}/secrets/oauth2`, { baseURL: remote });
425
+ const oauth2SecretsPath = join(configPath, 'secrets', 'oauth2.json');
426
+ if (existsSync(oauth2SecretsPath)) {
427
+ const [oauth2Secrets] = (await readData(oauth2SecretsPath));
428
+ if (oauth2Secrets.length) {
429
+ logger.info(`Publishing ${oauth2Secrets.length} app oauth2 secrets`);
430
+ for (const oauth2Secret of oauth2Secrets) {
431
+ const { authorizationUrl, clientId, clientSecret, name, tokenUrl, userInfoUrl, ...rest } = oauth2Secret;
432
+ const [parsedValues, missingValues] = parseValues('oauth2 secret', name, [
433
+ { authorizationUrl },
434
+ { tokenUrl },
435
+ { userInfoUrl },
436
+ { clientId },
437
+ { clientSecret },
438
+ ]);
439
+ if (missingValues) {
440
+ continue;
441
+ }
442
+ await axios.post(`/api/apps/${app.id}/secrets/oauth2`, { name, ...parsedValues, ...rest }, { baseURL: remote });
443
+ }
444
+ }
445
+ }
446
+ }
447
+ catch (error) {
448
+ logger.error('Something went wrong when creating app oauth2 secrets:');
449
+ logger.error(error);
450
+ }
451
+ try {
452
+ const sslSecretPath = join(configPath, 'secrets', 'ssl.json');
453
+ if (existsSync(sslSecretPath)) {
454
+ const [sslSecret] = (await readData(sslSecretPath));
455
+ if (sslSecret) {
456
+ logger.info('Publishing app ssl secret');
457
+ const { certificate, key } = sslSecret;
458
+ const [parsedValues, missingValues] = parseValues('ssl secret', '', [
459
+ { certificate },
460
+ { key },
461
+ ]);
462
+ if (missingValues) {
463
+ return;
464
+ }
465
+ await axios.put(`/api/apps/${app.id}/secrets/ssl`, { ...parsedValues }, { baseURL: remote });
466
+ }
467
+ }
468
+ }
469
+ catch (error) {
470
+ logger.error('Something went wrong when creating app ssl secret:');
471
+ logger.error(error);
472
+ }
473
+ try {
474
+ const scimSecretPath = join(configPath, 'secrets', 'scim.json');
475
+ if (existsSync(scimSecretPath)) {
476
+ const [scimSecret] = (await readData(scimSecretPath));
477
+ if (scimSecret) {
478
+ logger.info('Publishing app scim secret');
479
+ const { enabled, token } = scimSecret;
480
+ const [parsedValues, missingValues] = parseValues('scim secret', '', [{ token }]);
481
+ if (missingValues) {
482
+ return;
483
+ }
484
+ await axios.patch(`/api/apps/${app.id}/secrets/scim`, { enabled, ...parsedValues }, { baseURL: remote });
485
+ }
486
+ }
487
+ }
488
+ catch (error) {
489
+ logger.error('Something went wrong when creating app scim secret:');
490
+ logger.error(error);
491
+ }
492
+ }
493
+ else {
494
+ logger.warn(`Missing config directory in ${path}. Skipping...`);
495
+ }
496
+ }
304
497
  /**
305
498
  * @param path The path to the app directory.
306
499
  * @param languages A list of languages for which translations should be added in addition to the
@@ -452,15 +645,45 @@ This block version is not used in the app`);
452
645
  }
453
646
  }
454
647
  /**
455
- * Update an existing app.
648
+ * Resolve the app’s ID and remote based on the app context.
456
649
  *
457
- * @param argv The command line options used for updating the app.
650
+ * @param appPath The path to the app.
651
+ * @param name Which context to use in the AppsembleRC file.
652
+ * @param defaultRemote The remote to fall back to.
653
+ * @param defaultAppId The app ID to fall back to.
654
+ * @returns The resolved app ID and remote.
458
655
  */
459
- export async function updateApp({ clientCredentials, context, force, path, ...options }) {
460
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
656
+ export async function resolveAppIdAndRemote(appPath, name, defaultRemote, defaultAppId) {
657
+ var _a;
658
+ let id = defaultAppId;
659
+ let resolvedRemote = defaultRemote;
660
+ if (appPath) {
661
+ const rcPath = join(appPath, '.appsemblerc.yaml');
662
+ const [rc] = await readData(rcPath);
663
+ const context = (_a = rc.context) === null || _a === void 0 ? void 0 : _a[name];
664
+ if (!defaultAppId) {
665
+ id = context === null || context === void 0 ? void 0 : context.id;
666
+ }
667
+ if (context.remote) {
668
+ resolvedRemote = context.remote;
669
+ }
670
+ }
671
+ if (id == null) {
672
+ throw new AppsembleError(`App ID was not found in context.${name}.id nor in --app-id`);
673
+ }
674
+ return [id, coerceRemote(resolvedRemote)];
675
+ }
676
+ /**
677
+ * Publish a new app.
678
+ *
679
+ * @param options The options to use for publishing an app.
680
+ */
681
+ export async function publishApp({ clientCredentials, context, dryRun, modifyContext, path, ...options }) {
682
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
461
683
  const file = await stat(path);
462
684
  const formData = new FormData();
463
685
  const appsembleContext = await retrieveContext(path, context);
686
+ let rc;
464
687
  let yaml;
465
688
  let filename = relative(process.cwd(), path);
466
689
  const variant = (_b = (_a = appsembleContext.variant) !== null && _a !== void 0 ? _a : options.variant) !== null && _b !== void 0 ? _b : context;
@@ -473,16 +696,24 @@ export async function updateApp({ clientCredentials, context, force, path, ...op
473
696
  logger.warn(`App variant ${variant} is not defined in ${join(path, 'variants')}. Using default app.`);
474
697
  }
475
698
  if (file.isFile()) {
699
+ // Assuming file is App YAML
476
700
  const [, data] = await readData(appVariantPath);
477
701
  yaml = data;
478
702
  formData.append('yaml', data);
479
703
  }
480
704
  else {
481
- [, yaml] = await traverseAppDirectory(appVariantPath, formData);
705
+ let appsembleVariantContext;
706
+ [appsembleVariantContext, rc, yaml] = await traverseAppDirectory(appVariantPath, context, formData);
707
+ if (appsembleVariantContext.icon) {
708
+ appsembleContext.icon = appsembleVariantContext.icon;
709
+ }
710
+ if (appsembleVariantContext.maskableIcon) {
711
+ appsembleContext.maskableIcon = appsembleVariantContext.maskableIcon;
712
+ }
482
713
  filename = join(filename, 'app-definition.yaml');
483
714
  }
484
715
  const remote = (_c = appsembleContext.remote) !== null && _c !== void 0 ? _c : options.remote;
485
- const id = (_d = appsembleContext.id) !== null && _d !== void 0 ? _d : options.id;
716
+ const organizationId = (_d = appsembleContext.organization) !== null && _d !== void 0 ? _d : options.organization;
486
717
  const template = (_f = (_e = appsembleContext.template) !== null && _e !== void 0 ? _e : options.template) !== null && _f !== void 0 ? _f : false;
487
718
  const assetsClonable = (_h = (_g = appsembleContext.assetsClonable) !== null && _g !== void 0 ? _g : options.assetsClonable) !== null && _h !== void 0 ? _h : false;
488
719
  const demoMode = (_k = (_j = appsembleContext.demoMode) !== null && _j !== void 0 ? _j : options.demoMode) !== null && _k !== void 0 ? _k : false;
@@ -493,23 +724,23 @@ export async function updateApp({ clientCredentials, context, force, path, ...op
493
724
  const sentryDsn = (_q = appsembleContext.sentryDsn) !== null && _q !== void 0 ? _q : options.sentryDsn;
494
725
  const sentryEnvironment = (_r = appsembleContext.sentryEnvironment) !== null && _r !== void 0 ? _r : options.sentryEnvironment;
495
726
  const googleAnalyticsId = (_s = appsembleContext.googleAnalyticsId) !== null && _s !== void 0 ? _s : options.googleAnalyticsId;
496
- const resources = (_t = appsembleContext.resources) !== null && _t !== void 0 ? _t : options.resources;
497
- const assets = (_u = appsembleContext.assets) !== null && _u !== void 0 ? _u : options.assets;
498
- const { appLock } = appsembleContext;
499
- logger.info(`App id: ${id}`);
727
+ const assets = (_t = appsembleContext.assets) !== null && _t !== void 0 ? _t : options.assets;
728
+ const resources = (_u = appsembleContext.resources) !== null && _u !== void 0 ? _u : options.resources;
729
+ const appLock = appsembleContext.appLock || 'unlocked';
500
730
  logger.verbose(`App remote: ${remote}`);
731
+ logger.verbose(`App organization: ${organizationId}`);
501
732
  logger.verbose(`App is template: ${inspect(template, { colors: true })}`);
502
733
  logger.verbose(`App visibility: ${visibility}`);
503
734
  logger.verbose(`Icon background: ${iconBackground}`);
504
- logger.verbose(`Force update: ${inspect(force, { colors: true })}`);
505
- if (!id) {
506
- throw new AppsembleError('The app id must be passed as a command line flag or in the context');
735
+ if (!organizationId) {
736
+ throw new AppsembleError('An organization id must be passed as a command line flag or in the context');
507
737
  }
508
- formData.append('force', String(force));
738
+ formData.append('OrganizationId', organizationId);
509
739
  formData.append('template', String(template));
510
740
  formData.append('demoMode', String(demoMode));
511
741
  formData.append('visibility', visibility);
512
742
  formData.append('iconBackground', iconBackground);
743
+ formData.append('locked', appLock);
513
744
  if (icon) {
514
745
  const realIcon = typeof icon === 'string' ? createReadStream(icon) : icon;
515
746
  logger.info(`Using icon from ${(_v = realIcon.path) !== null && _v !== void 0 ? _v : 'stdin'}`);
@@ -536,21 +767,12 @@ export async function updateApp({ clientCredentials, context, force, path, ...op
536
767
  authScope += ' resources:write';
537
768
  }
538
769
  await authenticate(remote, authScope, clientCredentials);
539
- if (appLock) {
540
- logger.info(`Setting AppLock to ${appLock}`);
541
- try {
542
- await axios.post(`/api/apps/${id}/lock`, {
543
- locked: appLock,
544
- });
545
- }
546
- catch (error) {
547
- logger.error(error);
548
- process.exit(1);
549
- }
550
- }
551
770
  let data;
552
771
  try {
553
- ({ data } = await axios.patch(`/api/apps/${id}`, formData, { baseURL: remote }));
772
+ ({ data } = await axios.post('/api/apps', formData, {
773
+ baseURL: remote,
774
+ params: { dryRun: dryRun || undefined },
775
+ }));
554
776
  }
555
777
  catch (error) {
556
778
  if (!axios.isAxiosError(error)) {
@@ -561,38 +783,65 @@ export async function updateApp({ clientCredentials, context, force, path, ...op
561
783
  }
562
784
  throw new AppsembleError(printAxiosError(filename, yaml, error.response.data.data.errors));
563
785
  }
564
- if (file.isDirectory()) {
565
- // After uploading the app, upload block styles and messages if they are available
566
- await traverseBlockThemes(appVariantPath, data.id, remote, force);
567
- await uploadMessages(appVariantPath, data.id, remote, force);
568
- // After updating the app, publish seed resources and assets if they are available
569
- if (assets && (data.locked !== 'fullLock' || force)) {
786
+ if (dryRun) {
787
+ logger.info('Skipped uploading block themes and app messages.');
788
+ logger.info('Successfully ran dry run. The app should be valid when creating it.');
789
+ return;
790
+ }
791
+ if (file.isDirectory() && !dryRun) {
792
+ // After uploading the app, upload block styles, messages if they are available
793
+ await traverseBlockThemes(appVariantPath, data.id, remote, false);
794
+ await uploadMessages(appVariantPath, data.id, remote, false);
795
+ await publishAppConfig(appVariantPath, data, remote);
796
+ // After uploading the app, publish seed resources and assets if they are available
797
+ if (assets) {
570
798
  await publishSeedAssets(appVariantPath, data, remote, assetsClonable);
571
799
  }
572
- if (resources && (data.locked !== 'fullLock' || force)) {
800
+ if (resources) {
573
801
  await publishSeedResources(appVariantPath, data, remote);
574
802
  }
575
803
  }
804
+ if (modifyContext && appsembleContext && context && !dryRun) {
805
+ rc.context[context].id = data.id;
806
+ await writeData(join(appVariantPath, '.appsemblerc.yaml'), rc, { sort: false });
807
+ logger.info(`Updated .appsemblerc: Set context.${context}.id to ${data.id}`);
808
+ }
576
809
  const { host, protocol } = new URL(remote);
577
- logger.info(`Successfully updated app ${data.definition.name}! 🙌`);
810
+ logger.info(`Successfully published app ${data.definition.name}! 🙌`);
578
811
  logger.info(`App URL: ${protocol}//${data.path}.${data.OrganizationId}.${host}`);
579
812
  logger.info(`App store page: ${new URL(`/apps/${data.id}`, remote)}`);
580
- if (appVariantPath !== path) {
813
+ const rcCollections = (_0 = (_z = rc === null || rc === void 0 ? void 0 : rc.context) === null || _z === void 0 ? void 0 : _z[context]) === null || _0 === void 0 ? void 0 : _0.collections;
814
+ if (Array.isArray(rcCollections)) {
815
+ for (const collectionId of rcCollections) {
816
+ try {
817
+ await axios.post(`/api/appCollections/${collectionId}/apps`, { AppId: data.id }, {
818
+ baseURL: remote,
819
+ });
820
+ logger.info(`Successfully added app to collection ${collectionId}`);
821
+ }
822
+ catch (error) {
823
+ logger.error(error);
824
+ }
825
+ }
826
+ }
827
+ if (appVariantPath !== path && modifyContext && !dryRun) {
828
+ const [defaultAppRc] = await readData(join(path, '.appsemblerc.yaml'));
829
+ defaultAppRc.context[context] = rc.context[context];
830
+ await writeData(join(path, '.appsemblerc.yaml'), defaultAppRc, { sort: false });
581
831
  await rm(appVariantPath, { recursive: true });
582
832
  logger.info(`Removed ${appVariantPath}`);
583
833
  }
584
834
  }
585
835
  /**
586
- * Publish a new app.
836
+ * Update an existing app.
587
837
  *
588
- * @param options The options to use for publishing an app.
838
+ * @param argv The command line options used for updating the app.
589
839
  */
590
- export async function publishApp({ clientCredentials, context, dryRun, modifyContext, path, ...options }) {
591
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
840
+ export async function updateApp({ clientCredentials, context, force, path, ...options }) {
841
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
592
842
  const file = await stat(path);
593
843
  const formData = new FormData();
594
844
  const appsembleContext = await retrieveContext(path, context);
595
- let rc;
596
845
  let yaml;
597
846
  let filename = relative(process.cwd(), path);
598
847
  const variant = (_b = (_a = appsembleContext.variant) !== null && _a !== void 0 ? _a : options.variant) !== null && _b !== void 0 ? _b : context;
@@ -605,17 +854,23 @@ export async function publishApp({ clientCredentials, context, dryRun, modifyCon
605
854
  logger.warn(`App variant ${variant} is not defined in ${join(path, 'variants')}. Using default app.`);
606
855
  }
607
856
  if (file.isFile()) {
608
- // Assuming file is App YAML
609
857
  const [, data] = await readData(appVariantPath);
610
858
  yaml = data;
611
859
  formData.append('yaml', data);
612
860
  }
613
861
  else {
614
- [rc, yaml] = await traverseAppDirectory(appVariantPath, formData);
862
+ let appsembleVariantContext;
863
+ [appsembleVariantContext, , yaml] = await traverseAppDirectory(appVariantPath, context, formData);
864
+ if (appsembleVariantContext.icon) {
865
+ appsembleContext.icon = appsembleVariantContext.icon;
866
+ }
867
+ if (appsembleVariantContext.maskableIcon) {
868
+ appsembleContext.maskableIcon = appsembleVariantContext.maskableIcon;
869
+ }
615
870
  filename = join(filename, 'app-definition.yaml');
616
871
  }
617
872
  const remote = (_c = appsembleContext.remote) !== null && _c !== void 0 ? _c : options.remote;
618
- const organizationId = (_d = appsembleContext.organization) !== null && _d !== void 0 ? _d : options.organization;
873
+ const id = (_d = appsembleContext.id) !== null && _d !== void 0 ? _d : options.id;
619
874
  const template = (_f = (_e = appsembleContext.template) !== null && _e !== void 0 ? _e : options.template) !== null && _f !== void 0 ? _f : false;
620
875
  const assetsClonable = (_h = (_g = appsembleContext.assetsClonable) !== null && _g !== void 0 ? _g : options.assetsClonable) !== null && _h !== void 0 ? _h : false;
621
876
  const demoMode = (_k = (_j = appsembleContext.demoMode) !== null && _j !== void 0 ? _j : options.demoMode) !== null && _k !== void 0 ? _k : false;
@@ -626,23 +881,23 @@ export async function publishApp({ clientCredentials, context, dryRun, modifyCon
626
881
  const sentryDsn = (_q = appsembleContext.sentryDsn) !== null && _q !== void 0 ? _q : options.sentryDsn;
627
882
  const sentryEnvironment = (_r = appsembleContext.sentryEnvironment) !== null && _r !== void 0 ? _r : options.sentryEnvironment;
628
883
  const googleAnalyticsId = (_s = appsembleContext.googleAnalyticsId) !== null && _s !== void 0 ? _s : options.googleAnalyticsId;
629
- const assets = (_t = appsembleContext.assets) !== null && _t !== void 0 ? _t : options.assets;
630
- const resources = (_u = appsembleContext.resources) !== null && _u !== void 0 ? _u : options.resources;
631
- const appLock = appsembleContext.appLock || 'unlocked';
884
+ const resources = (_t = appsembleContext.resources) !== null && _t !== void 0 ? _t : options.resources;
885
+ const assets = (_u = appsembleContext.assets) !== null && _u !== void 0 ? _u : options.assets;
886
+ const { appLock } = appsembleContext;
887
+ logger.info(`App id: ${id}`);
632
888
  logger.verbose(`App remote: ${remote}`);
633
- logger.verbose(`App organization: ${organizationId}`);
634
889
  logger.verbose(`App is template: ${inspect(template, { colors: true })}`);
635
890
  logger.verbose(`App visibility: ${visibility}`);
636
891
  logger.verbose(`Icon background: ${iconBackground}`);
637
- if (!organizationId) {
638
- throw new AppsembleError('An organization id must be passed as a command line flag or in the context');
892
+ logger.verbose(`Force update: ${inspect(force, { colors: true })}`);
893
+ if (!id) {
894
+ throw new AppsembleError('The app id must be passed as a command line flag or in the context');
639
895
  }
640
- formData.append('OrganizationId', organizationId);
896
+ formData.append('force', String(force));
641
897
  formData.append('template', String(template));
642
898
  formData.append('demoMode', String(demoMode));
643
899
  formData.append('visibility', visibility);
644
900
  formData.append('iconBackground', iconBackground);
645
- formData.append('locked', appLock);
646
901
  if (icon) {
647
902
  const realIcon = typeof icon === 'string' ? createReadStream(icon) : icon;
648
903
  logger.info(`Using icon from ${(_v = realIcon.path) !== null && _v !== void 0 ? _v : 'stdin'}`);
@@ -669,12 +924,21 @@ export async function publishApp({ clientCredentials, context, dryRun, modifyCon
669
924
  authScope += ' resources:write';
670
925
  }
671
926
  await authenticate(remote, authScope, clientCredentials);
927
+ if (appLock) {
928
+ logger.info(`Setting AppLock to ${appLock}`);
929
+ try {
930
+ await axios.post(`/api/apps/${id}/lock`, {
931
+ locked: appLock,
932
+ });
933
+ }
934
+ catch (error) {
935
+ logger.error(error);
936
+ process.exit(1);
937
+ }
938
+ }
672
939
  let data;
673
940
  try {
674
- ({ data } = await axios.post('/api/apps', formData, {
675
- baseURL: remote,
676
- params: { dryRun: dryRun || undefined },
677
- }));
941
+ ({ data } = await axios.patch(`/api/apps/${id}`, formData, { baseURL: remote }));
678
942
  }
679
943
  catch (error) {
680
944
  if (!axios.isAxiosError(error)) {
@@ -685,83 +949,28 @@ export async function publishApp({ clientCredentials, context, dryRun, modifyCon
685
949
  }
686
950
  throw new AppsembleError(printAxiosError(filename, yaml, error.response.data.data.errors));
687
951
  }
688
- if (dryRun) {
689
- logger.info('Skipped uploading block themes and app messages.');
690
- logger.info('Successfully ran dry run. The app should be valid when creating it.');
691
- return;
692
- }
693
- if (file.isDirectory() && !dryRun) {
694
- // After uploading the app, upload block styles, messages if they are available
695
- await traverseBlockThemes(appVariantPath, data.id, remote, false);
696
- await uploadMessages(appVariantPath, data.id, remote, false);
697
- // After uploading the app, publish seed resources and assets if they are available
698
- if (assets) {
952
+ if (file.isDirectory()) {
953
+ // After uploading the app, upload block styles and messages if they are available
954
+ await traverseBlockThemes(appVariantPath, data.id, remote, force);
955
+ await uploadMessages(appVariantPath, data.id, remote, force);
956
+ await publishAppConfig(appVariantPath, data, remote);
957
+ // After updating the app, publish seed resources and assets if they are available
958
+ if (assets && (data.locked !== 'fullLock' || force)) {
699
959
  await publishSeedAssets(appVariantPath, data, remote, assetsClonable);
700
960
  }
701
- if (resources) {
961
+ if (resources && (data.locked !== 'fullLock' || force)) {
702
962
  await publishSeedResources(appVariantPath, data, remote);
703
963
  }
704
964
  }
705
- if (modifyContext && appsembleContext && context && !dryRun) {
706
- rc.context[context].id = data.id;
707
- await writeData(join(appVariantPath, '.appsemblerc.yaml'), rc, { sort: false });
708
- logger.info(`Updated .appsemblerc: Set context.${context}.id to ${data.id}`);
709
- }
710
965
  const { host, protocol } = new URL(remote);
711
- logger.info(`Successfully published app ${data.definition.name}! 🙌`);
966
+ logger.info(`Successfully updated app ${data.definition.name}! 🙌`);
712
967
  logger.info(`App URL: ${protocol}//${data.path}.${data.OrganizationId}.${host}`);
713
968
  logger.info(`App store page: ${new URL(`/apps/${data.id}`, remote)}`);
714
- const rcCollections = (_z = rc.context[context]) === null || _z === void 0 ? void 0 : _z.collections;
715
- if (Array.isArray(rcCollections)) {
716
- for (const collectionId of rcCollections) {
717
- try {
718
- await axios.post(`/api/appCollections/${collectionId}/apps`, { AppId: data.id }, {
719
- baseURL: remote,
720
- });
721
- logger.info(`Successfully added app to collection ${collectionId}`);
722
- }
723
- catch (error) {
724
- logger.error(error);
725
- }
726
- }
727
- }
728
- if (appVariantPath !== path && modifyContext && !dryRun) {
729
- const [defaultAppRc] = await readData(join(path, '.appsemblerc.yaml'));
730
- defaultAppRc.context[context] = rc.context[context];
731
- await writeData(join(path, '.appsemblerc.yaml'), defaultAppRc, { sort: false });
969
+ if (appVariantPath !== path) {
732
970
  await rm(appVariantPath, { recursive: true });
733
971
  logger.info(`Removed ${appVariantPath}`);
734
972
  }
735
973
  }
736
- /**
737
- * Resolve the app’s ID and remote based on the app context.
738
- *
739
- * @param appPath The path to the app.
740
- * @param name Which context to use in the AppsembleRC file.
741
- * @param defaultRemote The remote to fall back to.
742
- * @param defaultAppId The app ID to fall back to.
743
- * @returns The resolved app ID and remote.
744
- */
745
- export async function resolveAppIdAndRemote(appPath, name, defaultRemote, defaultAppId) {
746
- var _a;
747
- let id = defaultAppId;
748
- let resolvedRemote = defaultRemote;
749
- if (appPath) {
750
- const rcPath = join(appPath, '.appsemblerc.yaml');
751
- const [rc] = await readData(rcPath);
752
- const context = (_a = rc.context) === null || _a === void 0 ? void 0 : _a[name];
753
- if (!defaultAppId) {
754
- id = context === null || context === void 0 ? void 0 : context.id;
755
- }
756
- if (context.remote) {
757
- resolvedRemote = context.remote;
758
- }
759
- }
760
- if (id == null) {
761
- throw new AppsembleError(`App ID was not found in context.${name}.id nor in --app-id`);
762
- }
763
- return [id, coerceRemote(resolvedRemote)];
764
- }
765
974
  export async function patchApp({ id, remote, ...options }) {
766
975
  const formData = new FormData();
767
976
  if (options.path !== undefined) {