@swell/cli 1.0.2 → 2.0.1-alpha.0

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 +1 -1
  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 +177 -0
  17. package/dist/commands/app/push.d.ts +17 -0
  18. package/dist/commands/app/push.js +158 -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 +23 -0
  40. package/dist/commands/logs.js +207 -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 +117 -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 +167 -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 +40 -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 +20 -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 +367 -0
  97. package/dist/swell-command.d.ts +21 -0
  98. package/dist/swell-command.js +43 -0
  99. package/oclif.manifest.json +1085 -0
  100. package/package.json +89 -42
  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,302 @@
1
+ "use strict";
2
+ const origialConsoleLog = console.log;
3
+ addEventListener('fetch', (event) => {
4
+ event.respondWith(handleRequest(event.request, event.env, event));
5
+ });
6
+ async function handleRequest(originalRequest, _env, context) {
7
+ const req = new SwellRequest(originalRequest, context);
8
+ await req.initialize();
9
+ let response;
10
+ try {
11
+ response = await executeModuleHandler(req, context);
12
+ }
13
+ catch (err) {
14
+ // Log the error for Swell
15
+ console.error(err.stack);
16
+ response = new SwellResponse({ error: err.message }, { status: err.status || 500 });
17
+ }
18
+ // Create a response by default if one is not returned
19
+ if (!(response instanceof Response)) {
20
+ response = new SwellResponse(response);
21
+ }
22
+ // Send logs back to Swell
23
+ context.waitUntil(req.ingestLogs(response));
24
+ return response;
25
+ }
26
+ /**
27
+ * Invokes one of the function types that are available.
28
+ *
29
+ * Type-1: Named export function handlers
30
+ * ```
31
+ * export function post (req) {
32
+ *
33
+ * }
34
+ * ```
35
+ *
36
+ * Important note:
37
+ * - `delete` func cannot be implemented in Type-1 since it's a reserved name. So, consider using Type-3.
38
+ *
39
+ * Type-2: Default export function handlers
40
+ * ```
41
+ * export default function (req) {
42
+ *
43
+ * }
44
+ * ```
45
+ *
46
+ * Type-3: Default object with named function handlers
47
+ * ```
48
+ * export default {
49
+ * delete(req) {
50
+ *
51
+ * },
52
+ * post(req) {
53
+ *
54
+ * },
55
+ * }
56
+ * ```
57
+ *
58
+ * @param {SwellRequest} req
59
+ * @param {Event} context
60
+ */
61
+ async function executeModuleHandler(req, context) {
62
+ const method = req.method.toLowerCase();
63
+ const defaults = moduleExports.default;
64
+ if (moduleExports[method]) {
65
+ return moduleExports[method](req, context);
66
+ }
67
+ else if (defaults) {
68
+ if (typeof defaults === 'function') {
69
+ return defaults(req, context);
70
+ }
71
+ else if (defaults[method]) {
72
+ return defaults[method](req, context);
73
+ }
74
+ }
75
+ throw new Error(`Function does not export a method to handle ${method.toUpperCase()} requests`);
76
+ }
77
+ class SwellRequest {
78
+ constructor(req, context) {
79
+ this.originalRequest = req;
80
+ this.assignRequestProps(req);
81
+ // Set environment specific variables
82
+ this.context = context;
83
+ this.appId = req.headers.get('Swell-App-Id');
84
+ this.storeId = req.headers.get('Swell-Store-Id');
85
+ this.accessToken = req.headers.get('Swell-Access-Token');
86
+ this.publicKey = req.headers.get('Swell-Public-Key');
87
+ this.store = this.parseJson(req.headers.get('Swell-Store-Details'));
88
+ this.session = this.parseJson(req.headers.get('Swell-Session'));
89
+ this.logParams = this.parseJson(req.headers.get('Swell-Request-Log'));
90
+ this.apiHost = req.headers.get('Swell-API-Host') || 'https://api.schema.io';
91
+ this.id = req.headers.get('Swell-Request-ID') || this.logParams?.req_id;
92
+ // Swell client
93
+ this.swell = new SwellAPI(this, context);
94
+ // URL of the original request
95
+ this.url;
96
+ // Original body of the request, JSON if applicable
97
+ this.body = {};
98
+ // URL query parameters as an object
99
+ this.query = {};
100
+ // Combined object of body and query parameters
101
+ this.data = {};
102
+ // Internal logs
103
+ this._logs = [];
104
+ }
105
+ assignRequestProps(req) {
106
+ ['ur', 'method', 'headers', 'referrer', 'credentials'].forEach((prop) => {
107
+ this[prop] = req[prop];
108
+ });
109
+ }
110
+ async initialize() {
111
+ this.body = await this.originalRequest.text();
112
+ try {
113
+ this.data = JSON.parse(this.body);
114
+ this.body = { ...this.data };
115
+ }
116
+ catch (err) {
117
+ this.data = {};
118
+ }
119
+ this.url = new URL(this.originalRequest.url);
120
+ // Convert the query parameters to an object
121
+ this.url.searchParams.forEach((value, key) => {
122
+ this.query[key] = value;
123
+ this.data[key] = value;
124
+ });
125
+ // Bind the console methods to the request
126
+ console.log = this.log.bind(this, 'info');
127
+ console.info = this.log.bind(this, 'info');
128
+ console.debug = this.log.bind(this, 'debug');
129
+ console.warn = this.log.bind(this, 'warn');
130
+ console.error = this.log.bind(this, 'error');
131
+ }
132
+ parseJson(input) {
133
+ try {
134
+ return JSON.parse(input);
135
+ }
136
+ catch (err) {
137
+ return {};
138
+ }
139
+ }
140
+ log(level, ...line) {
141
+ origialConsoleLog(...line);
142
+ this._logs.push({
143
+ date: Date.now(),
144
+ line: line.map((l) => JSON.stringify(l)),
145
+ ...(level !== 'info' ? { level } : {}),
146
+ });
147
+ }
148
+ async ingestLogs(response) {
149
+ if (!this.logParams) {
150
+ return;
151
+ }
152
+ if (this.logParams.$start) {
153
+ this.logParams.time = Date.now() - this.logParams.$start;
154
+ delete this.logParams.$start;
155
+ }
156
+ const result = await this.swell.post('/:logs', {
157
+ $ingest_function_logs: {
158
+ params: {
159
+ ...this.logParams,
160
+ message: {
161
+ ...this.logParams?.message,
162
+ logs: this._logs,
163
+ status: response.status,
164
+ },
165
+ },
166
+ },
167
+ });
168
+ if (!result?.success) {
169
+ console.error('Error ingesting logs', result);
170
+ }
171
+ }
172
+ }
173
+ class SwellAPI {
174
+ constructor(req, context) {
175
+ this.request = req;
176
+ this.baseUrl = req.apiHost;
177
+ this.basicAuth = `${req.storeId}:${req.accessToken}`;
178
+ this.context = context;
179
+ }
180
+ toBase64(inputString) {
181
+ const utf8Bytes = new TextEncoder().encode(inputString);
182
+ let base64String = '';
183
+ for (let i = 0; i < utf8Bytes.length; i += 3) {
184
+ const chunk = utf8Bytes.slice(i, i + 3);
185
+ base64String += btoa(String.fromCharCode(...chunk));
186
+ }
187
+ return base64String;
188
+ }
189
+ stringifyQuery(queryObject, prefix) {
190
+ const result = [];
191
+ for (const [key, value] of Object.entries(queryObject)) {
192
+ const prefixKey = prefix ? `${prefix}[${key}]` : key;
193
+ const isObject = value !== null && typeof value === 'object';
194
+ const encodedResult = isObject
195
+ ? this.stringifyQuery(value, prefixKey)
196
+ : `${encodeURIComponent(prefixKey)}=${encodeURIComponent(value)}`;
197
+ result.push(encodedResult);
198
+ }
199
+ return result.join('&');
200
+ }
201
+ async makeRequest(method, url, data) {
202
+ const requestOptions = {
203
+ method,
204
+ headers: {
205
+ Authorization: `Basic ${this.toBase64(this.basicAuth)}`,
206
+ 'User-Agent': 'swell-functions/1.0',
207
+ 'Content-Type': 'application/json',
208
+ ...(this.request.id ? { 'Swell-Request-ID': this.request.id } : {}),
209
+ },
210
+ };
211
+ let query = '';
212
+ if (data) {
213
+ try {
214
+ if (method === 'GET') {
215
+ query = `?${this.stringifyQuery(data)}`;
216
+ }
217
+ else {
218
+ requestOptions.body = JSON.stringify(data);
219
+ requestOptions.headers['Content-Length'] = requestOptions.body.length;
220
+ }
221
+ }
222
+ catch {
223
+ throw new Error(`Error serializing data: ${data}`);
224
+ }
225
+ }
226
+ const endpointUrl = String(url).startsWith('/') ? url.substring(1) : url;
227
+ const response = await fetch(`${this.baseUrl}/${endpointUrl}${query}`, requestOptions);
228
+ const responseText = await response.text();
229
+ let result;
230
+ try {
231
+ result = JSON.parse(responseText);
232
+ }
233
+ catch {
234
+ result = String(responseText || '').trim();
235
+ }
236
+ if (response.status > 299) {
237
+ throw new SwellError(result, {
238
+ status: response.status,
239
+ method,
240
+ endpointUrl,
241
+ });
242
+ }
243
+ else if (method !== 'GET' && result?.errors) {
244
+ throw new SwellError(result.errors, { status: 400, method, endpointUrl });
245
+ }
246
+ return result;
247
+ }
248
+ async get(url, query) {
249
+ return this.makeRequest('GET', url, query);
250
+ }
251
+ async put(url, data) {
252
+ return this.makeRequest('PUT', url, data);
253
+ }
254
+ async post(url, data) {
255
+ return this.makeRequest('POST', url, data);
256
+ }
257
+ async delete(url, data) {
258
+ return this.makeRequest('DELETE', url, data);
259
+ }
260
+ async settings(id = this.request.appId) {
261
+ return this.makeRequest('GET', `/settings/${id}`);
262
+ }
263
+ }
264
+ class SwellError extends Error {
265
+ constructor(message, options = {}) {
266
+ let formattedMessage;
267
+ if (typeof message === 'string') {
268
+ formattedMessage = message;
269
+ }
270
+ else {
271
+ formattedMessage = JSON.stringify(message, null, 2);
272
+ }
273
+ if (options.method && options.endpointUrl) {
274
+ formattedMessage = `${options.method} /${options.endpointUrl}\n${formattedMessage}`;
275
+ }
276
+ super(formattedMessage);
277
+ this.name = 'SwellError';
278
+ this.status = options.status || 500;
279
+ }
280
+ }
281
+ class SwellResponse extends Response {
282
+ constructor(data, options = {}) {
283
+ const resultHeaders = {};
284
+ let result;
285
+ if (typeof data === 'string') {
286
+ result = data;
287
+ resultHeaders['Content-Type'] = 'text/plain;charset=UTF-8';
288
+ }
289
+ else if (data !== undefined) {
290
+ result = JSON.stringify(data, null, 2);
291
+ resultHeaders['Content-Type'] = 'application/json;charset=UTF-8';
292
+ }
293
+ super(result?.toString?.('utf-8'), {
294
+ status: 200,
295
+ ...options,
296
+ headers: {
297
+ ...resultHeaders,
298
+ ...(options.headers || {}),
299
+ },
300
+ });
301
+ }
302
+ }
@@ -0,0 +1,36 @@
1
+ import { AppCommand } from './app-command.js';
2
+ import { App, AppConfig, AppVersion } from './lib/apps/index.js';
3
+ /**
4
+ * Extends the AppCommand with methods that ease managing remote apps and
5
+ * configurations.
6
+ */
7
+ export declare abstract class RemoteAppCommand extends AppCommand {
8
+ static orientation: AppCommandOrientation;
9
+ app: App;
10
+ dashboardUrl(flags?: {
11
+ [flag: string]: any;
12
+ }, storeId?: string): string;
13
+ protected deleteConfig(configId: string): Promise<any>;
14
+ protected getApp(id?: string): Promise<App>;
15
+ protected getAppWithConfig(id?: string, errorNotFound?: boolean): Promise<App>;
16
+ protected getCreateUpdateApp(updateApp?: App): Promise<App>;
17
+ protected handleRequestErrors(request: () => any, spinnerError?: () => void): Promise<any>;
18
+ init(): Promise<void>;
19
+ protected postConfig(configJson: any): Promise<any>;
20
+ protected pushConfigFile(appConfig: AppConfig): Promise<any>;
21
+ protected removeConfig(config: AppConfig): Promise<any>;
22
+ protected showErrors(errors: any, spinnerError?: () => void): void;
23
+ protected syncAppConfigs(): Promise<{
24
+ installed: string[];
25
+ removed: string[];
26
+ }>;
27
+ /**
28
+ * Reads the .swellrc file and sets up the remote app configuration.
29
+ *
30
+ * @returns App
31
+ */
32
+ private initApp;
33
+ private showOrientation;
34
+ protected getVersions(version?: string | undefined | null): Promise<AppVersion[]>;
35
+ protected swellRcPath(): string;
36
+ }
@@ -0,0 +1,367 @@
1
+ import { inflect } from 'inflection';
2
+ import snakeCase from 'lodash/snakeCase.js';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+ import ora from 'ora';
6
+ import { AppCommand } from './app-command.js';
7
+ import { appConfigFileExists, readRcFile, writeRcFile, } from './lib/app-config.js';
8
+ import { FunctionProcessingError, IgnoringFileError, allConfigFilesPaths, appConfigFromFile, appLogoIcon, batchAppConfigsByType, configFilePath, isAppLogoIcon, } from './lib/apps/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 defined 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) {
101
+ app = await this.getAppWithConfig(swellId);
102
+ }
103
+ }
104
+ return app;
105
+ }
106
+ async getCreateUpdateApp(updateApp) {
107
+ const spinner = ora();
108
+ const remoteApp = await readRcFile(this.swellRcPath());
109
+ /* eslint-disable camelcase */
110
+ const body = {
111
+ ...this.appConfig.store,
112
+ // we store locally the private id as the id, but we need to switch it
113
+ // when posting to the API
114
+ id: undefined,
115
+ logo_icon: null,
116
+ private_id: this.app?.private_id,
117
+ ...(remoteApp?.id ? { source_id: remoteApp.id } : undefined),
118
+ };
119
+ const logoIcon = appLogoIcon(this.appPath);
120
+ if (logoIcon) {
121
+ const icon = fs.readFileSync(logoIcon, { encoding: 'base64' });
122
+ body.logo_icon = icon;
123
+ }
124
+ /* eslint-enable camelcase */
125
+ let app = updateApp || {};
126
+ let appCreated = false;
127
+ if (!app.id) {
128
+ // May already have a dev instance
129
+ const existingDevApp = await this.api.get({
130
+ adminPath: `/apps/${this.app?.private_id}`,
131
+ });
132
+ spinner.start(`Creating app...`);
133
+ if (existingDevApp) {
134
+ app = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${existingDevApp.id}` }, { body }), () => spinner.fail('Error creating app'));
135
+ }
136
+ else {
137
+ app = await this.handleRequestErrors(async () => this.api.post({ adminPath: '/apps' }, { body }), () => spinner.fail('Error creating app'));
138
+ }
139
+ appCreated = true;
140
+ }
141
+ else if (app.id) {
142
+ spinner.start(`Updating app...`);
143
+ app = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${app.id}` }, { body }), () => spinner.fail('Error updating app'));
144
+ }
145
+ if (app?.id) {
146
+ await writeRcFile(this.swellRcPath(), { id: app.id });
147
+ // if not failed, we want to display correct messaging
148
+ if (spinner.isSpinning) {
149
+ if (appCreated) {
150
+ spinner.succeed('Development app created.');
151
+ this.log();
152
+ }
153
+ else {
154
+ spinner.stop();
155
+ }
156
+ this.log('App details');
157
+ spinner.succeed('app details');
158
+ if (logoIcon) {
159
+ spinner.succeed('app logo');
160
+ }
161
+ }
162
+ this.log();
163
+ return app;
164
+ }
165
+ return {};
166
+ }
167
+ async handleRequestErrors(request, spinnerError) {
168
+ try {
169
+ const response = await request();
170
+ if (response?.status !== 200 && response?.errors) {
171
+ this.showErrors(response.errors, spinnerError);
172
+ this.error('API request failed');
173
+ }
174
+ return response;
175
+ }
176
+ catch (error) {
177
+ let jsonError;
178
+ try {
179
+ jsonError = JSON.parse(error.message);
180
+ }
181
+ catch {
182
+ // noop, likely JSON parse error
183
+ }
184
+ if (jsonError && jsonError.error) {
185
+ this.showErrors(jsonError, spinnerError);
186
+ this.error('API request failed');
187
+ }
188
+ throw error;
189
+ }
190
+ }
191
+ async init() {
192
+ await super.init();
193
+ this.app = this.initApp();
194
+ const klass = this.ctor;
195
+ const { flags } = await this.parse(klass);
196
+ if (klass.orientation) {
197
+ this.showOrientation(klass.orientation, flags);
198
+ }
199
+ }
200
+ async postConfig(configJson) {
201
+ if (!this.app) {
202
+ this.error('App is not defined');
203
+ }
204
+ this.debug('postConfig: configJson', configJson);
205
+ return this.api.post({ adminPath: `/apps/${this.app.id}/configs` }, { body: configJson });
206
+ }
207
+ async pushConfigFile(appConfig) {
208
+ const { localPath } = appConfig;
209
+ const spinner = ora();
210
+ let postData;
211
+ if (!localPath) {
212
+ throw new Error('Missing local config path');
213
+ }
214
+ // we don't want to treat the app logo asset as config file
215
+ // it is dealt with at the app level
216
+ if (isAppLogoIcon(localPath)) {
217
+ return;
218
+ }
219
+ const pathDesc = localPath.split('/').slice(-2).join('/');
220
+ spinner.start(`Pushing ${pathDesc}`);
221
+ try {
222
+ postData = await appConfig.postData();
223
+ }
224
+ catch (error) {
225
+ if (error instanceof IgnoringFileError) {
226
+ spinner.fail(`Ignoring file: ${error.message} in ${localPath}`);
227
+ return;
228
+ }
229
+ if (error instanceof FunctionProcessingError) {
230
+ this.debug(error);
231
+ this.debug(error.originalError);
232
+ spinner.fail(`${error.message} ${error.originalError?.message?.toLowerCase()}`);
233
+ return;
234
+ }
235
+ spinner.fail(error.message);
236
+ throw error;
237
+ }
238
+ const result = await this.handleRequestErrors(async () => this.postConfig(postData), () => spinner.fail(`Error pushing ${pathDesc}`));
239
+ spinner.succeed(`Pushed ${pathDesc}`);
240
+ return result;
241
+ }
242
+ async removeConfig(config) {
243
+ const spinner = ora();
244
+ spinner.start(`Removing ${config.type} ${config.name}`);
245
+ const result = await this.handleRequestErrors(async () => this.deleteConfig(config.id), () => spinner.fail(`Error removing ${config.type} ${config.name}`));
246
+ spinner.warn(`Removed ${config.type} ${config.name}`);
247
+ return result;
248
+ }
249
+ showErrors(errors, spinnerError) {
250
+ const actualErrors = Object.keys(errors).filter((err) => Object.prototype.hasOwnProperty.call(errors, err));
251
+ if (spinnerError) {
252
+ spinnerError();
253
+ }
254
+ else {
255
+ this.logError(inflect('Error', actualErrors.length));
256
+ }
257
+ for (const err of actualErrors) {
258
+ // we need to display `id` instead of `private_id`
259
+ // developers don't have any knowledge of private ids
260
+ const displayErr = err === 'private_id' ? 'id' : err;
261
+ this.logError([
262
+ ' -',
263
+ `${displayErr}:`.toLowerCase().replace(/^error:/, ''),
264
+ errors[err].message,
265
+ ]
266
+ .filter(Boolean)
267
+ .join(' '));
268
+ }
269
+ }
270
+ async syncAppConfigs() {
271
+ const configsToPush = [];
272
+ const installed = [];
273
+ const removed = [];
274
+ for (const { configFile, configType } of allConfigFilesPaths(this.appPath)) {
275
+ // skip logo icon as it is dealt with at the app level
276
+ if (isAppLogoIcon(configFile)) {
277
+ continue;
278
+ }
279
+ const appConfig = appConfigFromFile(configFile, configType);
280
+ configsToPush.push(appConfig);
281
+ }
282
+ const batches = batchAppConfigsByType(configsToPush);
283
+ for (const batch of batches) {
284
+ this.log(style.appConfigName(batch.label));
285
+ // eslint-disable-next-line no-await-in-loop
286
+ for (const appConfig of batch.configs) {
287
+ const installedConfig = await this.pushConfigFile(appConfig);
288
+ if (installedConfig)
289
+ installed.push(installedConfig.id);
290
+ }
291
+ this.log();
292
+ }
293
+ // pull the app again after the latest push
294
+ // this allows us to get the latest configs updates that the remote app
295
+ // might have done
296
+ this.app = await this.getAppWithConfig(this.app.id);
297
+ this.debugJson('syncAppConfigs: app', this.app);
298
+ // if the remote app has configs that are not in the local app, we want to
299
+ // remove them
300
+ for (const config of this.app?.configs || []) {
301
+ const appConfig = config;
302
+ if (!appConfig.localPath) {
303
+ // eslint-disable-next-line no-await-in-loop
304
+ await this.removeConfig(appConfig);
305
+ removed.push(appConfig.id);
306
+ }
307
+ }
308
+ return { installed, removed };
309
+ }
310
+ /**
311
+ * Reads the .swellrc file and sets up the remote app configuration.
312
+ *
313
+ * @returns App
314
+ */
315
+ initApp() {
316
+ let remoteApp = {};
317
+ try {
318
+ this.debug('initApp: swellRcPath', this.swellRcPath());
319
+ remoteApp = readRcFile(this.swellRcPath());
320
+ this.debug('initApp: remoteApp', remoteApp);
321
+ }
322
+ catch (error) {
323
+ // if the file doesn't exist, we don't want to throw an error as we might
324
+ // want to create it later.
325
+ if (error.code !== 'ENOENT') {
326
+ throw error;
327
+ }
328
+ }
329
+ if (this.appConfig.get('id')) {
330
+ // eslint-disable-next-line camelcase
331
+ remoteApp.private_id = snakeCase(this.appConfig.get('id'));
332
+ }
333
+ return remoteApp;
334
+ }
335
+ showOrientation(orientation, flags) {
336
+ const orientationOutput = [];
337
+ const currentStore = swellConfig.getDefaultStore();
338
+ const { env, store } = flags;
339
+ if (store) {
340
+ orientationOutput.push(`[store] ${style.storeId(store)}`);
341
+ }
342
+ else if (currentStore) {
343
+ orientationOutput.push(`[store] ${style.storeId(currentStore)}`);
344
+ }
345
+ const outputEnv = env || orientation.env;
346
+ if (outputEnv) {
347
+ const outputStyle = outputEnv === 'live' ? style.appEnvLive : style.appEnv;
348
+ orientationOutput.push(`[env] ${outputStyle(outputEnv)}`);
349
+ }
350
+ this.log(orientationOutput.join(' '));
351
+ this.log();
352
+ }
353
+ async getVersions(version) {
354
+ const versionsResponse = await this.api.get({ adminPath: `/apps/${this.app.id}/versions` }, {
355
+ query: {
356
+ limit: 1000,
357
+ sort: 'version desc',
358
+ ...(version ? { version } : undefined),
359
+ },
360
+ });
361
+ this.debugJson('getApp: versionsResponse', versionsResponse);
362
+ return versionsResponse.results || [];
363
+ }
364
+ swellRcPath() {
365
+ return path.join(this.appPath, '.swellrc');
366
+ }
367
+ }