@swell/cli 2.9.13 → 2.9.15

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.
@@ -17,8 +17,8 @@ type to an app in your store's test environment.
17
17
  If the app does not exist, it will be created and its global ID saved to a .swellrc file.
18
18
 
19
19
  - If no file is specified, all configuration files will be pushed to the store.
20
- This includes the app icon (assets/icon.png), cover image (assets/image.png),
21
- and swell.json.
20
+ This includes the app icon (assets/icon.png), screenshots (assets/screenshots/),
21
+ README.md, and swell.json.
22
22
  - If a file is specified, only that file will be pushed to the store.
23
23
  - If a directory is specified, only files in that directory will be pushed.
24
24
 
@@ -115,10 +115,17 @@ The app must be connected to a partner account with access to publish.`;
115
115
  const spinner = ora();
116
116
  this.log();
117
117
  spinner.start(`${amend ? 'Updating release' : 'Releasing'} version ${style.appConfigValue(appVersion.version)}`);
118
- await this.updateVersion(appVersion.version, {
119
- ...releaseData,
120
- released: true,
121
- }, spinner);
118
+ try {
119
+ await this.updateVersion(appVersion.version, {
120
+ ...releaseData,
121
+ released: true,
122
+ });
123
+ }
124
+ catch (error) {
125
+ spinner.fail(error.message);
126
+ this.logReleaseDetailsHint(error);
127
+ return this.exit(1);
128
+ }
122
129
  spinner.succeed(`${style.appConfigValue(this.app.name)} release ${appVersion.version} ${amend ? 'updated' : 'released'}.`);
123
130
  }
124
131
  async validateCreateVersion(inputVersion, flags) {
@@ -138,7 +145,8 @@ The app must be connected to a partner account with access to publish.`;
138
145
  else {
139
146
  await this.config.runCommand('app:version', [
140
147
  version,
141
- ...(flags.message ? [`-m ${JSON.stringify(flags.message)}`] : []),
148
+ '--validate-release',
149
+ ...(flags.message ? ['-m', flags.message] : []),
142
150
  ...(flags['no-git-tag'] ? ['--no-git-tag'] : []),
143
151
  ...(flags.yes ? ['-y'] : []),
144
152
  ]);
@@ -10,6 +10,7 @@ export default class AppVersion extends PushAppCommand {
10
10
  'force-create-files': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
11
  message: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
12
  'no-git-tag': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
13
+ 'validate-release': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
13
14
  yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
14
15
  };
15
16
  static orientation: {
@@ -64,6 +64,10 @@ behavior by using the --no-git-tag option.`;
64
64
  'no-git-tag': Flags.boolean({
65
65
  description: 'do not create a git tag for this version',
66
66
  }),
67
+ 'validate-release': Flags.boolean({
68
+ description: 'validate the app has all details required to release before creating the version',
69
+ hidden: true,
70
+ }),
67
71
  yes: Flags.boolean({
68
72
  char: 'y',
69
73
  description: 'skip confirmation prompt',
@@ -75,7 +79,7 @@ behavior by using the --no-git-tag option.`;
75
79
  static summary = 'Increment the version of your Swell app.';
76
80
  async run() {
77
81
  const { args, flags } = await this.parse(AppVersion);
78
- const { amend, 'no-git-tag': noGitTag, yes, 'force-create-files': forceCreateFiles = false, } = flags;
82
+ const { amend, 'no-git-tag': noGitTag, yes, 'force-create-files': forceCreateFiles = false, 'validate-release': validateRelease = false, } = flags;
79
83
  let { message } = flags;
80
84
  let { nextVersion } = args;
81
85
  if (!(await this.ensureAppExists(undefined, false))) {
@@ -116,7 +120,7 @@ behavior by using the --no-git-tag option.`;
116
120
  }
117
121
  return;
118
122
  }
119
- nextVersion = this.validateAndComputeVersion(nextVersion, localVersion);
123
+ nextVersion = await this.validateAndComputeVersion(nextVersion, localVersion);
120
124
  await this.pushAppConfigs();
121
125
  await this.deployAppFrontend(false, false);
122
126
  if (!message) {
@@ -139,18 +143,33 @@ behavior by using the --no-git-tag option.`;
139
143
  const spinner = ora();
140
144
  spinner.start('Creating version');
141
145
  try {
142
- await this.createRemoteVersion(nextVersion, message, spinner, forceCreateFiles);
143
- await this.gitCommit(message);
144
- if (!noGitTag)
145
- await this.gitTag(nextVersion);
146
+ await this.createRemoteVersion(nextVersion, message, spinner, {
147
+ forceCreateFiles,
148
+ validateRelease,
149
+ });
146
150
  }
147
151
  catch (error) {
148
152
  spinner.fail(error.stderr || error.message);
149
- return;
153
+ this.logReleaseDetailsHint(error);
154
+ return this.exit(1);
150
155
  }
151
156
  spinner.succeed(`${style.appConfigValue(this.app.name)} version ${nextVersion} created.`);
157
+ // git is optional: commit and tag only when the app is inside a repository
158
+ if (await git.find({ cwd: this.appPath })) {
159
+ try {
160
+ await this.gitCommit(message);
161
+ if (!noGitTag)
162
+ await this.gitTag(nextVersion);
163
+ }
164
+ catch (error) {
165
+ this.log(style.funcWarn(`Git commit/tag failed: ${(error.stderr || error.message).trim()}`));
166
+ }
167
+ }
168
+ else {
169
+ this.log(style.dim('Not a git repository, skipping git commit and tag.'));
170
+ }
152
171
  }
153
- async createRemoteVersion(nextVersion, message, spinner, forceCreateFiles = false) {
172
+ async createRemoteVersion(nextVersion, message, spinner, { forceCreateFiles = false, validateRelease = false } = {}) {
154
173
  const body = {
155
174
  description: message,
156
175
  version: nextVersion,
@@ -158,6 +177,9 @@ behavior by using the --no-git-tag option.`;
158
177
  if (forceCreateFiles) {
159
178
  body.$force_create_files = true;
160
179
  }
180
+ if (validateRelease) {
181
+ body.$validate_release = true;
182
+ }
161
183
  // update the version on the remote
162
184
  const versionResponse = await this.api.post({ adminPath: `/apps/${this.app.id}/versions` }, {
163
185
  body,
@@ -173,34 +195,40 @@ behavior by using the --no-git-tag option.`;
173
195
  this.swellConfig.set('version', nextVersion);
174
196
  }
175
197
  async gitCommit(message) {
198
+ const cwd = this.appPath;
176
199
  // check if there are changes in the app config file
177
200
  // if there are no changes, there is nothing to commit
178
201
  const configStatus = await git.spawn(['status', this.swellConfig.path], {
179
- cwd: this.swellConfig.cwd,
202
+ cwd,
180
203
  });
181
204
  if (configStatus.stdout.includes('nothing to commit')) {
182
205
  return;
183
206
  }
184
207
  // add the app config file to commit - this will contain the new version
185
- await git.spawn(['add', this.swellConfig.path]);
208
+ await git.spawn(['add', this.swellConfig.path], { cwd });
186
209
  // commit the version change to git. It skips the git hooks with -n
187
- await git.spawn(['commit', '-n', '-m', message]);
210
+ await git.spawn(['commit', '-n', '-m', message], { cwd });
188
211
  }
189
212
  async gitTag(nextVersion) {
190
213
  const tag = nextVersion;
191
214
  // tag the version in git
192
- await git.spawn(['tag', tag], { cwd: process.cwd() });
215
+ await git.spawn(['tag', tag], { cwd: this.appPath });
193
216
  }
194
- validateAndComputeVersion(nextVersion, localVersion) {
217
+ async validateAndComputeVersion(nextVersion, localVersion) {
195
218
  let version = nextVersion;
196
219
  // compute the value of the next version and check if it's valid
197
220
  if (RELEASE_TYPES.includes(nextVersion)) {
198
221
  // increment the version
199
222
  version = semver.inc(localVersion, version);
200
223
  }
201
- if (semver.lte(version, localVersion)) {
224
+ if (semver.lt(version, localVersion)) {
202
225
  this.error(`Version ${version} must be greater than the current version ${localVersion}.`);
203
226
  }
227
+ // swell.json may declare a version that has not been created yet,
228
+ // e.g. the first version of a new app. Only reject it if it already exists.
229
+ if (semver.eq(version, localVersion) && (await this.getVersion(version))) {
230
+ this.error(`Version ${version} already exists.`);
231
+ }
204
232
  return version;
205
233
  }
206
234
  }
package/dist/lib/api.d.ts CHANGED
@@ -21,7 +21,7 @@ export default class Api {
21
21
  put(pathOpts: Api.Paths, options?: Api.RequestOptions): Promise<any>;
22
22
  delete(pathOpts: Api.Paths, options?: Api.RequestOptions): Promise<any>;
23
23
  isTestEnvEnabled(): Promise<boolean>;
24
- setEnv(envId: string): Promise<void>;
24
+ setEnv(envId: string, storeId?: string): Promise<void>;
25
25
  setStoreEnv(storeId: string, envId?: string): Promise<void>;
26
26
  setPublicKey(publicKey?: string): Promise<void>;
27
27
  private isFrontend;
package/dist/lib/api.js CHANGED
@@ -2,7 +2,7 @@ import fetch from 'node-fetch';
2
2
  import { stringify } from 'qs';
3
3
  import { getCurrentAppSlugId, hasAppContext } from './apps/index.js';
4
4
  import config from './config.js';
5
- import { API_BASE_URL, getAdminApiBaseUrl, getFrontendApiBaseUrl, } from './constants.js';
5
+ import { API_BASE_URL, getAdminApiBaseUrl, getFrontendApiBaseUrl, getLoginHost, } from './constants.js';
6
6
  import style from './style.js';
7
7
  export var HttpMethod;
8
8
  (function (HttpMethod) {
@@ -190,16 +190,21 @@ export default class Api {
190
190
  });
191
191
  return clientRecord.test_enabled !== false;
192
192
  }
193
- async setEnv(envId) {
193
+ async setEnv(envId, storeId) {
194
194
  const hasTestEnv = await this.isTestEnvEnabled();
195
195
  if (!hasTestEnv) {
196
- throw new Error('Test environment is not enabled for this store.');
196
+ const targetStoreId = storeId || this.storeId || config.getDefaultStore();
197
+ const settingsUrl = `${getLoginHost(targetStoreId)}/admin/settings/general`;
198
+ throw new Error(`Test environment is not enabled for this store.
199
+
200
+ Enable it in the dashboard under Settings > General > Test environment:
201
+ ${settingsUrl}`);
197
202
  }
198
203
  this.envId = envId;
199
204
  }
200
205
  async setStoreEnv(storeId, envId) {
201
206
  if (envId) {
202
- await this.setEnv(envId);
207
+ await this.setEnv(envId, storeId);
203
208
  }
204
209
  this.storeId = storeId;
205
210
  this.envId = envId;
@@ -16,7 +16,6 @@ export interface App {
16
16
  [key: string]: string;
17
17
  };
18
18
  configs?: AppConfig[];
19
- cover_image?: unknown;
20
19
  date_created?: string;
21
20
  date_updated?: string;
22
21
  demo_url?: string;
@@ -39,6 +38,10 @@ export interface App {
39
38
  title: string;
40
39
  }>;
41
40
  id?: string;
41
+ images?: Array<{
42
+ file?: unknown;
43
+ id: string;
44
+ }>;
42
45
  installed?: {
43
46
  date_created: string;
44
47
  id: string;
@@ -76,6 +76,7 @@ export declare abstract class PushAppCommand extends RemoteAppCommand {
76
76
  }): void;
77
77
  logStorefrontConnected(): void;
78
78
  logStorefrontFrontendUrl(storefront?: any, branchId?: string): void;
79
+ logReleaseDetailsHint(error: any): void;
79
80
  pushFile(relativePath: string): Promise<void>;
80
81
  pushFilePath(relativePath: string, force?: boolean): Promise<void>;
81
82
  resolveAppPath(appId?: string, targetPath?: string): Promise<string>;
@@ -672,6 +672,14 @@ export class PushAppCommand extends RemoteAppCommand {
672
672
  const currentStore = localConfig.getDefaultStore();
673
673
  this.log(`View your storefront at ${style.link(this.storefrontFrontendUrl(currentStore, storefront, branchId))}.`);
674
674
  }
675
+ // Tell the user how to recover when the server rejects a release
676
+ // because app details are missing
677
+ logReleaseDetailsHint(error) {
678
+ if (!String(error?.message).includes('to release')) {
679
+ return;
680
+ }
681
+ this.log(style.dim(`Update swell.json and assets as needed, run ${style.command('swell app push')}, then run ${style.command('swell app release')} again.`));
682
+ }
675
683
  async pushFile(relativePath) {
676
684
  const relativePathPosix = toPosixPath(relativePath);
677
685
  const basePath = relativePathPosix.split('/')[0];
@@ -1180,7 +1180,7 @@
1180
1180
  "name": "file"
1181
1181
  }
1182
1182
  },
1183
- "description": "Push all app files, a specific file, or a specific configuration\ntype to an app in your store's test environment.\n\nIf the app does not exist, it will be created and its global ID saved to a .swellrc file.\n\n- If no file is specified, all configuration files will be pushed to the store.\n This includes the app icon (assets/icon.png), cover image (assets/image.png),\n and swell.json.\n- If a file is specified, only that file will be pushed to the store.\n- If a directory is specified, only files in that directory will be pushed.\n\nApp file directories:\nassets/\ncontent/\nfrontend/\ncomponents/\nfunctions/\nmodels/\nnotifications/\nsettings/\ntheme/\nwebhooks/",
1183
+ "description": "Push all app files, a specific file, or a specific configuration\ntype to an app in your store's test environment.\n\nIf the app does not exist, it will be created and its global ID saved to a .swellrc file.\n\n- If no file is specified, all configuration files will be pushed to the store.\n This includes the app icon (assets/icon.png), screenshots (assets/screenshots/),\n README.md, and swell.json.\n- If a file is specified, only that file will be pushed to the store.\n- If a directory is specified, only files in that directory will be pushed.\n\nApp file directories:\nassets/\ncontent/\nfrontend/\ncomponents/\nfunctions/\nmodels/\nnotifications/\nsettings/\ntheme/\nwebhooks/",
1184
1184
  "examples": [
1185
1185
  "swell app push",
1186
1186
  "swell app push content",
@@ -1359,6 +1359,13 @@
1359
1359
  "allowNo": false,
1360
1360
  "type": "boolean"
1361
1361
  },
1362
+ "validate-release": {
1363
+ "description": "validate the app has all details required to release before creating the version",
1364
+ "hidden": true,
1365
+ "name": "validate-release",
1366
+ "allowNo": false,
1367
+ "type": "boolean"
1368
+ },
1362
1369
  "yes": {
1363
1370
  "char": "y",
1364
1371
  "description": "skip confirmation prompt",
@@ -3594,5 +3601,5 @@
3594
3601
  ]
3595
3602
  }
3596
3603
  },
3597
- "version": "2.9.13"
3604
+ "version": "2.9.15"
3598
3605
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swell/cli",
3
- "version": "2.9.13",
3
+ "version": "2.9.15",
4
4
  "type": "module",
5
5
  "description": "Swell's command line interface/utility",
6
6
  "keywords": [