@swell/cli 1.0.2 → 2.0.1-alpha.10

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.
Files changed (105) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +2 -140
  3. package/bin/dev.cmd +3 -0
  4. package/bin/dev.js +7 -0
  5. package/bin/run.cmd +3 -0
  6. package/bin/run.js +7 -0
  7. package/dist/app-command.d.ts +22 -0
  8. package/dist/app-command.js +45 -0
  9. package/dist/commands/app/_pull.d.ts +8 -0
  10. package/dist/commands/app/_pull.js +45 -0
  11. package/dist/commands/app/_uninstall.d.ts +12 -0
  12. package/dist/commands/app/_uninstall.js +45 -0
  13. package/dist/commands/app/info.d.ts +15 -0
  14. package/dist/commands/app/info.js +92 -0
  15. package/dist/commands/app/install.d.ts +17 -0
  16. package/dist/commands/app/install.js +179 -0
  17. package/dist/commands/app/push.d.ts +20 -0
  18. package/dist/commands/app/push.js +170 -0
  19. package/dist/commands/app/version.d.ts +18 -0
  20. package/dist/commands/app/version.js +156 -0
  21. package/dist/commands/app/watch.d.ts +15 -0
  22. package/dist/commands/app/watch.js +110 -0
  23. package/dist/commands/create/app.d.ts +21 -0
  24. package/dist/commands/create/app.js +210 -0
  25. package/dist/commands/create/content.d.ts +19 -0
  26. package/dist/commands/create/content.js +87 -0
  27. package/dist/commands/create/function.d.ts +25 -0
  28. package/dist/commands/create/function.js +191 -0
  29. package/dist/commands/create/index.d.ts +6 -0
  30. package/dist/commands/create/index.js +34 -0
  31. package/dist/commands/create/model.d.ts +19 -0
  32. package/dist/commands/create/model.js +87 -0
  33. package/dist/commands/create/notification.d.ts +25 -0
  34. package/dist/commands/create/notification.js +170 -0
  35. package/dist/commands/login.d.ts +22 -0
  36. package/dist/commands/login.js +77 -0
  37. package/dist/commands/logout.d.ts +8 -0
  38. package/dist/commands/logout.js +45 -0
  39. package/dist/commands/logs.d.ts +24 -0
  40. package/dist/commands/logs.js +212 -0
  41. package/dist/commands/switch.d.ts +10 -0
  42. package/dist/commands/switch.js +41 -0
  43. package/dist/commands/whoami.d.ts +7 -0
  44. package/dist/commands/whoami.js +24 -0
  45. package/dist/create-collection-command.d.ts +26 -0
  46. package/dist/create-collection-command.js +97 -0
  47. package/dist/create-command.d.ts +16 -0
  48. package/dist/create-command.js +59 -0
  49. package/dist/index.d.ts +1 -0
  50. package/dist/index.js +1 -0
  51. package/dist/lib/api.d.ts +13 -0
  52. package/dist/lib/api.js +126 -0
  53. package/dist/lib/app-config.d.ts +29 -0
  54. package/dist/lib/app-config.js +127 -0
  55. package/dist/lib/apps/app-config.d.ts +42 -0
  56. package/dist/lib/apps/app-config.js +170 -0
  57. package/dist/lib/apps/index.d.ts +67 -0
  58. package/dist/lib/apps/index.js +167 -0
  59. package/dist/lib/apps/paths.d.ts +18 -0
  60. package/dist/lib/apps/paths.js +54 -0
  61. package/dist/lib/bundle.d.ts +1 -0
  62. package/dist/lib/bundle.js +43 -0
  63. package/dist/lib/config.d.ts +37 -0
  64. package/dist/lib/config.js +131 -0
  65. package/dist/lib/constants.d.ts +5 -0
  66. package/dist/lib/constants.js +17 -0
  67. package/dist/lib/create/content.d.ts +23 -0
  68. package/dist/lib/create/content.js +40 -0
  69. package/dist/lib/create/function.d.ts +17 -0
  70. package/dist/lib/create/function.js +36 -0
  71. package/dist/lib/create/index.d.ts +13 -0
  72. package/dist/lib/create/index.js +22 -0
  73. package/dist/lib/create/model.d.ts +22 -0
  74. package/dist/lib/create/model.js +30 -0
  75. package/dist/lib/create/notification.d.ts +25 -0
  76. package/dist/lib/create/notification.js +1 -0
  77. package/dist/lib/info.d.ts +38 -0
  78. package/dist/lib/info.js +91 -0
  79. package/dist/lib/logs/index.d.ts +23 -0
  80. package/dist/lib/logs/index.js +88 -0
  81. package/dist/lib/logs/line-output.d.ts +5 -0
  82. package/dist/lib/logs/line-output.js +13 -0
  83. package/dist/lib/logs/output.d.ts +7 -0
  84. package/dist/lib/logs/output.js +8 -0
  85. package/dist/lib/logs/table-output.d.ts +11 -0
  86. package/dist/lib/logs/table-output.js +128 -0
  87. package/dist/lib/sessions.d.ts +9 -0
  88. package/dist/lib/sessions.js +110 -0
  89. package/dist/lib/stores.d.ts +63 -0
  90. package/dist/lib/stores.js +173 -0
  91. package/dist/lib/style.d.ts +24 -0
  92. package/dist/lib/style.js +29 -0
  93. package/dist/lib/swell-function-wrapper.d.ts +88 -0
  94. package/dist/lib/swell-function-wrapper.js +302 -0
  95. package/dist/remote-app-command.d.ts +36 -0
  96. package/dist/remote-app-command.js +388 -0
  97. package/dist/swell-command.d.ts +21 -0
  98. package/dist/swell-command.js +43 -0
  99. package/oclif.manifest.json +1090 -0
  100. package/package.json +81 -43
  101. package/LICENSE +0 -674
  102. package/dist/bin/multipart-parser-85ef9626.js +0 -456
  103. package/dist/bin/swell-0f71f07d.js +0 -30670
  104. package/dist/bin/swell.js +0 -24
  105. package/dist/man/swell.1 +0 -85
@@ -0,0 +1,388 @@
1
+ import { inflect } from 'inflection';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import ora from 'ora';
5
+ import { AppCommand } from './app-command.js';
6
+ import { appConfigFileExists, readRcFile, writeRcFile, } from './lib/app-config.js';
7
+ import { FunctionProcessingError, IgnoringFileError, allConfigFilesPaths, appConfigFromFile, appLogoIcon, batchAppConfigsByType, configFilePath, isAppLogoIcon, } from './lib/apps/index.js';
8
+ import { toAppId } from './lib/create/index.js';
9
+ import { default as swellConfig } from './lib/config.js';
10
+ import { getLoginHost } from './lib/constants.js';
11
+ import style from './lib/style.js';
12
+ /**
13
+ * Extends the AppCommand with methods that ease managing remote apps and
14
+ * configurations.
15
+ */
16
+ export class RemoteAppCommand extends AppCommand {
17
+ // allows child commands to set orientation
18
+ // most of the times this will be the target environment
19
+ static orientation;
20
+ // an API instance of the current app definition
21
+ app = {};
22
+ dashboardUrl(flags = {}, storeId) {
23
+ const { env } = flags;
24
+ const envFlag = env || this.ctor.orientation?.env;
25
+ const outputEnv = envFlag === 'live' ? '' : !envFlag ? '' : 'test';
26
+ return `${getLoginHost(storeId)}/admin/${outputEnv ? `${outputEnv}/` : ''}apps/${this.app.id}`;
27
+ }
28
+ async deleteConfig(configId) {
29
+ if (!this.app) {
30
+ this.error('App is not defined');
31
+ }
32
+ return this.api.delete({
33
+ adminPath: `/apps/${this.app.id}/configs/${configId}`,
34
+ });
35
+ }
36
+ async getApp(id = '') {
37
+ const app = await this.api.get({ adminPath: `/apps/${id}` });
38
+ // Get app configs for processing
39
+ app.configs = [];
40
+ const configsResponse = await this.api.get({ adminPath: `/apps/${id}/configs` }, { query: { limit: 1000 } });
41
+ this.debugJson('getApp: configsResponse', configsResponse);
42
+ // for each config, we check if there's a local file and we add it to the
43
+ // AppConfig object
44
+ for (const config of configsResponse.results || []) {
45
+ const localPath = configFilePath(this.appPath, config);
46
+ if (appConfigFileExists(localPath)) {
47
+ config.localPath = localPath;
48
+ }
49
+ app.configs.push(config);
50
+ }
51
+ this.debugJson('getApp: remote app', app);
52
+ return app;
53
+ }
54
+ async getAppWithConfig(id = '', errorNotFound = true) {
55
+ const swellId = this.app?.id;
56
+ const appConfigId = this.appConfig.get('id');
57
+ if (!appConfigId) {
58
+ this.error(`App ${style.appConfigName('id')} is not defined in swell.json.`);
59
+ }
60
+ let appId = id || swellId;
61
+ if (!appId) {
62
+ const privateId = this.app?.private_id;
63
+ if (!privateId) {
64
+ this.error(`App ${style.appConfigName('id')} is not defined in swell.json.`);
65
+ }
66
+ if (privateId !== appConfigId) {
67
+ this.error(`App ${style.appConfigName('id')} must be declared in snake_case: ${style.appConfigValue(appConfigId)}.`);
68
+ }
69
+ appId = privateId;
70
+ }
71
+ // we initialize the app with the id so we can use it in the API calls
72
+ let app = {};
73
+ try {
74
+ app = await this.getApp(appId);
75
+ }
76
+ catch (error) {
77
+ this.debug('getApp: app not found', error);
78
+ let errorJson;
79
+ try {
80
+ errorJson = JSON.parse(error.message);
81
+ }
82
+ catch {
83
+ // if there's a JSON parse error, throw the original error
84
+ this.error(error);
85
+ }
86
+ // if the app doesn't exist, we don't want to throw an error as we might
87
+ // want to create it.
88
+ //
89
+ // if the `id` was passed as argument though, we want to throw an error so
90
+ // we don't create a loop.
91
+ //
92
+ // we don't throw an error ever sometimes, like when pushing an app
93
+ const shouldThrow = !errorNotFound && (errorJson.error.message !== 'App not found' || id);
94
+ if (shouldThrow) {
95
+ const errorMessage = typeof errorJson.error === 'string'
96
+ ? errorJson.error
97
+ : errorJson.error.message;
98
+ this.error(errorMessage);
99
+ }
100
+ if (swellId && swellId !== id && swellId !== this.app?.id) {
101
+ app = await this.getAppWithConfig(swellId);
102
+ }
103
+ }
104
+ return app;
105
+ }
106
+ async getCreateUpdateApp(updateApp) {
107
+ const spinner = ora();
108
+ let remoteApp;
109
+ try {
110
+ remoteApp = await readRcFile(this.swellRcPath());
111
+ }
112
+ catch (error) {
113
+ if (error.code !== 'ENOENT') {
114
+ throw error;
115
+ }
116
+ }
117
+ /* eslint-disable camelcase */
118
+ const body = {
119
+ ...this.appConfig.store,
120
+ // we store locally the private id as the id, but we need to switch it
121
+ // when posting to the API
122
+ id: undefined,
123
+ logo_icon: null,
124
+ ...(this.appConfig.store.id
125
+ ? { private_id: this.appConfig.store.id }
126
+ : undefined),
127
+ ...(remoteApp?.id ? { source_id: remoteApp.id } : undefined),
128
+ };
129
+ const logoIcon = appLogoIcon(this.appPath);
130
+ if (logoIcon) {
131
+ const icon = fs.readFileSync(logoIcon, { encoding: 'base64' });
132
+ body.logo_icon = icon;
133
+ }
134
+ /* eslint-enable camelcase */
135
+ let app = updateApp || {};
136
+ let appCreated = false;
137
+ if (!app.id) {
138
+ // May already have a dev instance
139
+ let existingDevApp;
140
+ try {
141
+ existingDevApp = await this.api.get({
142
+ adminPath: `/apps/${this.appConfig.store.id}`,
143
+ });
144
+ }
145
+ catch (err) {
146
+ // noop
147
+ }
148
+ spinner.start(`Creating app...`);
149
+ if (existingDevApp) {
150
+ app = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${existingDevApp.id}` }, { body }), () => spinner.fail('Error creating app'));
151
+ }
152
+ else {
153
+ app = await this.handleRequestErrors(async () => this.api.post({ adminPath: '/apps' }, { body }), () => spinner.fail('Error creating app'));
154
+ appCreated = true;
155
+ }
156
+ }
157
+ else if (app.id) {
158
+ spinner.start(`Updating app...`);
159
+ app = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${app.id}` }, { body }), () => spinner.fail('Error updating app'));
160
+ }
161
+ if (app?.id) {
162
+ await writeRcFile(this.swellRcPath(), { id: app.id });
163
+ // if not failed, we want to display correct messaging
164
+ if (spinner.isSpinning) {
165
+ if (appCreated) {
166
+ spinner.succeed('Development app created.');
167
+ this.log();
168
+ }
169
+ else {
170
+ spinner.stop();
171
+ }
172
+ this.log('App details');
173
+ spinner.succeed('app details');
174
+ if (logoIcon) {
175
+ spinner.succeed('app logo');
176
+ }
177
+ }
178
+ this.log();
179
+ return app;
180
+ }
181
+ return {};
182
+ }
183
+ async handleRequestErrors(request, spinnerError) {
184
+ try {
185
+ const response = await request();
186
+ if (response?.status !== 200 && response?.errors) {
187
+ this.showErrors(response.errors, spinnerError);
188
+ this.error('API request failed');
189
+ }
190
+ return response;
191
+ }
192
+ catch (error) {
193
+ let jsonError;
194
+ try {
195
+ jsonError = JSON.parse(error.message);
196
+ }
197
+ catch {
198
+ // noop, likely JSON parse error
199
+ }
200
+ if (jsonError && jsonError.error) {
201
+ this.showErrors(jsonError, spinnerError);
202
+ this.error('API request failed');
203
+ }
204
+ throw error;
205
+ }
206
+ }
207
+ async init() {
208
+ await super.init();
209
+ this.app = this.initApp();
210
+ const klass = this.ctor;
211
+ const { flags } = await this.parse(klass);
212
+ if (klass.orientation) {
213
+ this.showOrientation(klass.orientation, flags);
214
+ }
215
+ }
216
+ async postConfig(configJson) {
217
+ if (!this.app) {
218
+ this.error('App is not defined');
219
+ }
220
+ this.debug('postConfig: configJson', configJson);
221
+ return this.api.post({ adminPath: `/apps/${this.app.id}/configs` }, { body: configJson });
222
+ }
223
+ async pushConfigFile(appConfig) {
224
+ const { localPath } = appConfig;
225
+ const spinner = ora();
226
+ let postData;
227
+ if (!localPath) {
228
+ throw new Error('Missing local config path');
229
+ }
230
+ // we don't want to treat the app logo asset as config file
231
+ // it is dealt with at the app level
232
+ if (isAppLogoIcon(localPath)) {
233
+ return;
234
+ }
235
+ const pathDesc = localPath.split('/').slice(-2).join('/');
236
+ spinner.start(`Pushing ${pathDesc}`);
237
+ try {
238
+ postData = await appConfig.postData();
239
+ if (!postData) {
240
+ spinner.stop();
241
+ return;
242
+ }
243
+ }
244
+ catch (error) {
245
+ if (error instanceof IgnoringFileError) {
246
+ spinner.fail(`Ignoring file: ${error.message} in ${localPath}`);
247
+ return;
248
+ }
249
+ if (error instanceof FunctionProcessingError) {
250
+ this.debug(error);
251
+ this.debug(error.originalError);
252
+ spinner.fail(`${error.message} ${error.originalError?.message?.toLowerCase()}`);
253
+ return;
254
+ }
255
+ spinner.fail(error.message);
256
+ throw error;
257
+ }
258
+ const result = await this.handleRequestErrors(async () => this.postConfig(postData), () => spinner.fail(`Error pushing ${pathDesc}`));
259
+ spinner.succeed(`Pushed ${pathDesc}`);
260
+ return result;
261
+ }
262
+ async removeConfig(config) {
263
+ const spinner = ora();
264
+ spinner.start(`Removing ${config.type} ${config.name}`);
265
+ const result = await this.handleRequestErrors(async () => this.deleteConfig(config.id), () => spinner.fail(`Error removing ${config.type} ${config.name}`));
266
+ spinner.warn(`Removed ${config.type} ${config.name}`);
267
+ return result;
268
+ }
269
+ showErrors(errors, spinnerError) {
270
+ const actualErrors = Object.keys(errors).filter((err) => Object.prototype.hasOwnProperty.call(errors, err));
271
+ if (spinnerError) {
272
+ spinnerError();
273
+ }
274
+ else {
275
+ this.logError(inflect('Error', actualErrors.length));
276
+ }
277
+ for (const err of actualErrors) {
278
+ // we need to display `id` instead of `private_id`
279
+ // developers don't have any knowledge of private ids
280
+ const displayErr = err === 'private_id' ? 'id' : err;
281
+ this.logError([
282
+ ' -',
283
+ `${displayErr}:`.toLowerCase().replace(/^error:/, ''),
284
+ errors[err].message,
285
+ ]
286
+ .filter(Boolean)
287
+ .join(' '));
288
+ }
289
+ }
290
+ async syncAppConfigs() {
291
+ const configsToPush = [];
292
+ const installed = [];
293
+ const removed = [];
294
+ for (const { configFile, configType } of allConfigFilesPaths(this.appPath)) {
295
+ // skip logo icon as it is dealt with at the app level
296
+ if (isAppLogoIcon(configFile)) {
297
+ continue;
298
+ }
299
+ const appConfig = appConfigFromFile(configFile, configType);
300
+ configsToPush.push(appConfig);
301
+ }
302
+ const batches = batchAppConfigsByType(configsToPush);
303
+ for (const batch of batches) {
304
+ this.log(style.appConfigName(batch.label));
305
+ // eslint-disable-next-line no-await-in-loop
306
+ for (const appConfig of batch.configs) {
307
+ const installedConfig = await this.pushConfigFile(appConfig);
308
+ if (installedConfig)
309
+ installed.push(installedConfig.id);
310
+ }
311
+ this.log();
312
+ }
313
+ // pull the app again after the latest push
314
+ // this allows us to get the latest configs updates that the remote app
315
+ // might have done
316
+ this.app = await this.getAppWithConfig(this.app.id);
317
+ this.debugJson('syncAppConfigs: app', this.app);
318
+ // if the remote app has configs that are not in the local app, we want to
319
+ // remove them
320
+ for (const config of this.app?.configs || []) {
321
+ const appConfig = config;
322
+ if (!appConfig.localPath) {
323
+ // eslint-disable-next-line no-await-in-loop
324
+ await this.removeConfig(appConfig);
325
+ removed.push(appConfig.id);
326
+ }
327
+ }
328
+ return { installed, removed };
329
+ }
330
+ /**
331
+ * Reads the .swellrc file and sets up the remote app configuration.
332
+ *
333
+ * @returns App
334
+ */
335
+ initApp() {
336
+ let remoteApp = {};
337
+ try {
338
+ this.debug('initApp: swellRcPath', this.swellRcPath());
339
+ remoteApp = readRcFile(this.swellRcPath());
340
+ this.debug('initApp: remoteApp', remoteApp);
341
+ }
342
+ catch (error) {
343
+ // if the file doesn't exist, we don't want to throw an error as we might
344
+ // want to create it later.
345
+ if (error.code !== 'ENOENT') {
346
+ throw error;
347
+ }
348
+ }
349
+ if (this.appConfig.get('id')) {
350
+ // use the same formula as the backend api
351
+ // eslint-disable-next-line camelcase
352
+ remoteApp.private_id = toAppId(this.appConfig.get('id'));
353
+ }
354
+ return remoteApp;
355
+ }
356
+ showOrientation(orientation, flags) {
357
+ const orientationOutput = [];
358
+ const currentStore = swellConfig.getDefaultStore();
359
+ const { env, store } = flags;
360
+ if (store) {
361
+ orientationOutput.push(`[store] ${style.storeId(store)}`);
362
+ }
363
+ else if (currentStore) {
364
+ orientationOutput.push(`[store] ${style.storeId(currentStore)}`);
365
+ }
366
+ const outputEnv = env || orientation.env;
367
+ if (outputEnv) {
368
+ const outputStyle = outputEnv === 'live' ? style.appEnvLive : style.appEnv;
369
+ orientationOutput.push(`[env] ${outputStyle(outputEnv)}`);
370
+ }
371
+ this.log(orientationOutput.join(' '));
372
+ this.log();
373
+ }
374
+ async getVersions(version) {
375
+ const versionsResponse = await this.api.get({ adminPath: `/apps/${this.app.id}/versions` }, {
376
+ query: {
377
+ limit: 1000,
378
+ sort: 'version desc',
379
+ ...(version ? { version } : undefined),
380
+ },
381
+ });
382
+ this.debugJson('getApp: versionsResponse', versionsResponse);
383
+ return versionsResponse.results || [];
384
+ }
385
+ swellRcPath() {
386
+ return path.join(this.appPath, '.swellrc');
387
+ }
388
+ }
@@ -0,0 +1,21 @@
1
+ import { Command, Config } from '@oclif/core';
2
+ import Api from './lib/api.js';
3
+ /**
4
+ * A base class for Swell CLI commands.
5
+ *
6
+ * This class extends the oclif Command class and adds:
7
+ *
8
+ * - an Api instance
9
+ * - logging methods
10
+ * - a catch method to handle errors
11
+ */
12
+ export declare abstract class SwellCommand extends Command {
13
+ static examples: Command.Example[];
14
+ api: Api;
15
+ requiresAuth: boolean;
16
+ constructor(argv: string[], config: Config);
17
+ catch(error: any): Promise<void>;
18
+ debugJson(...args: any[]): void;
19
+ logError(message: string): void;
20
+ logSuccess(message: string): void;
21
+ }
@@ -0,0 +1,43 @@
1
+ import git from '@npmcli/git';
2
+ import { Command } from '@oclif/core';
3
+ import { FetchError } from 'node-fetch';
4
+ import Api from './lib/api.js';
5
+ import style from './lib/style.js';
6
+ const { GitUnknownError } = git.errors;
7
+ /**
8
+ * A base class for Swell CLI commands.
9
+ *
10
+ * This class extends the oclif Command class and adds:
11
+ *
12
+ * - an Api instance
13
+ * - logging methods
14
+ * - a catch method to handle errors
15
+ */
16
+ export class SwellCommand extends Command {
17
+ static examples = ['<%= config.bin %> <%= command.id %>'];
18
+ api;
19
+ requiresAuth = true;
20
+ constructor(argv, config) {
21
+ super(argv, config);
22
+ this.api = new Api();
23
+ }
24
+ async catch(error) {
25
+ if (error instanceof GitUnknownError) {
26
+ this.error(style.error(`git: ${error.stderr || error.stdout || error.message}`));
27
+ }
28
+ else if (error instanceof FetchError) {
29
+ this.error(`Could not connect to Swell API. Please try again later: ${error.message}`);
30
+ }
31
+ throw error;
32
+ }
33
+ debugJson(...args) {
34
+ const stringified = args.map((arg) => typeof arg === 'string' ? arg : JSON.stringify(arg, null, 4));
35
+ this.debug(...stringified);
36
+ }
37
+ logError(message) {
38
+ this.log(`${style.error(message)}`);
39
+ }
40
+ logSuccess(message) {
41
+ this.log(`${style.success(message)}`);
42
+ }
43
+ }