@shoper/cli 0.9.4-7 → 0.9.6-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.
@@ -56,7 +56,44 @@ export const toInquirerControl = (control) => {
56
56
  }
57
57
  };
58
58
  export const toInquirerValidate = (validators) => {
59
+ if (!validators || validators.length === 0)
60
+ return undefined;
59
61
  return (value) => {
60
- return validators?.find((v) => !v.isValid(value))?.getErrorMessage() ?? true;
62
+ for (const v of validators) {
63
+ if (typeof v?.isValid === 'function') {
64
+ if (!v.isValid(value)) {
65
+ return typeof v.getErrorMessage === 'function' ? v.getErrorMessage() : 'Validation failed';
66
+ }
67
+ continue;
68
+ }
69
+ switch (v?.type) {
70
+ case 'length': {
71
+ const strValue = String(value ?? '');
72
+ const { min, max } = v.options ?? {};
73
+ if (typeof min === 'number' && strValue.length < min) {
74
+ return `Minimum length is ${min} characters`;
75
+ }
76
+ if (typeof max === 'number' && strValue.length > max) {
77
+ return `Maximum length is ${max} characters`;
78
+ }
79
+ break;
80
+ }
81
+ case 'greaterEqThan': {
82
+ const numValue = Number(value);
83
+ const { min } = v.options ?? {};
84
+ if (typeof min === 'number' && (isNaN(numValue) || numValue < min)) {
85
+ return `Value must be greater than or equal to ${min}`;
86
+ }
87
+ break;
88
+ }
89
+ case 'required': {
90
+ if (value === null || value === undefined || String(value).trim() === '') {
91
+ return 'This field is required';
92
+ }
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ return true;
61
98
  };
62
99
  };
@@ -1,4 +1,4 @@
1
1
  import inquirer from 'inquirer';
2
2
  export const Form = ({ controls, onSubmit }) => {
3
- inquirer.prompt(controls).then(onSubmit);
3
+ return inquirer.prompt(controls).then(onSubmit);
4
4
  };
@@ -1,4 +1,4 @@
1
- import { Args } from '@oclif/core';
1
+ import { Args, Flags } from '@oclif/core';
2
2
  import { BaseThemeCommand } from '../../class/base_theme_command.js';
3
3
  import { CLI_AUTH_API_NAME } from '../../../cli/auth/cli_auth_constants.js';
4
4
  import { THEME_ACTIONS_API_NAME, THEME_ACTIONS_TYPES } from '../../features/theme/actions/theme_actions_constants.js';
@@ -7,20 +7,41 @@ import { renderOnce } from '../../../ui/ui_utils.js';
7
7
  import { MissingCredentialsError } from '../../../cli/commands/auth/ui/missing_credentials_error.js';
8
8
  import React from 'react';
9
9
  import { MissingThemeIdError } from '../ui/missing_theme_id_error.js';
10
- import { THEME_SKINSTORE_API_NAME } from '../../features/theme/skinstore/theme_publish_constants.js';
10
+ import { THEME_SKINSTORE_API_NAME, RELEASE_TYPE_TEST, RELEASE_TYPE_STABLE, IMAGE_MAX_SIZE, IMAGE_ALLOWED_EXTENSIONS } from '../../features/theme/skinstore/theme_publish_constants.js';
11
11
  import { Form } from '../../../cli/features/controls/ui/form.js';
12
+ import { toInquirerControls } from '../../../cli/features/controls/ui/controls_mappers.js';
12
13
  import { ThemeError } from '../ui/theme_error.js';
13
14
  import { LOGGER_API_NAME } from '../../../cli/utilities/features/logger/logger_constants.js';
15
+ import { Text } from '../../../ui/text.js';
16
+ import { Box } from '../../../ui/box.js';
17
+ import { Success } from '../../../ui/message_box/success.js';
18
+ import { Info } from '../../../ui/message_box/info.js';
19
+ import { Error as ErrorBox } from '../../../ui/message_box/error.js';
20
+ import { UnpermittedCommandError } from '../ui/unpermitted_command_error.js';
21
+ import { promptConfirmation } from '../../../ui/prompts/prompt_confirmation.js';
22
+ import fs from 'fs';
23
+ import path from 'path';
14
24
  export class ThemePublishCommand extends BaseThemeCommand {
15
- static summary = 'Permanently deletes the specified theme from your store.';
16
- static description = 'This action cannot be undone, so make sure you really want to remove this theme.\n\nYou can run this command from a specific theme directory (ID not needed) or outside any theme directory (theme ID required).';
25
+ static summary = 'Publish theme to SkinStore.';
26
+ static description = 'Publishes a theme to the Shoper SkinStore marketplace.\n\nFor first-time publication, you will be prompted to fill in theme details.\nFor already published themes, this performs an upgrade (new version).';
17
27
  static examples = [
18
28
  {
19
- description: 'This will delete the theme with ID 123 permanently from your store. Make sure you have a backup if needed.',
29
+ description: 'Publish current theme (from theme directory)',
30
+ command: '<%= config.bin %> <%= command.id %>'
31
+ },
32
+ {
33
+ description: 'Publish specific theme by ID',
20
34
  command: '<%= config.bin %> <%= command.id %> 123'
35
+ },
36
+ {
37
+ description: 'Publish as test release',
38
+ command: '<%= config.bin %> <%= command.id %> --release test'
39
+ },
40
+ {
41
+ description: 'Publish with screenshots from directory',
42
+ command: '<%= config.bin %> <%= command.id %> --images ./screenshots'
21
43
  }
22
44
  ];
23
- static hidden = true;
24
45
  static args = {
25
46
  id: Args.string({
26
47
  description: 'Theme id',
@@ -29,6 +50,18 @@ export class ThemePublishCommand extends BaseThemeCommand {
29
50
  type: 'string'
30
51
  })
31
52
  };
53
+ static flags = {
54
+ release: Flags.string({
55
+ char: 'r',
56
+ description: 'Release type',
57
+ options: ['test', 'stable'],
58
+ default: 'stable'
59
+ }),
60
+ images: Flags.string({
61
+ char: 'i',
62
+ description: 'Path to directory with screenshots'
63
+ })
64
+ };
32
65
  async run() {
33
66
  const themeId = this.args.id;
34
67
  const cliAuthApi = this.getApi(CLI_AUTH_API_NAME);
@@ -43,38 +76,192 @@ export class ThemePublishCommand extends BaseThemeCommand {
43
76
  try {
44
77
  let _themeId = themeId;
45
78
  if (executionContext.type !== EXECUTION_CONTEXTS.theme && !_themeId) {
79
+ renderOnce(React.createElement(MissingThemeIdError, null,
80
+ React.createElement(Box, { flexDirection: "column", gap: 1 },
81
+ React.createElement(Text, null, "Usage: shoper theme publish [ID]"),
82
+ React.createElement(Text, null, "Please run this command inside a theme directory or provide a theme ID."))));
46
83
  return;
47
84
  }
48
- if (executionContext.type === EXECUTION_CONTEXTS.theme) {
85
+ if (executionContext.type === EXECUTION_CONTEXTS.theme)
49
86
  _themeId = _themeId ?? executionContext.themeId;
50
- }
51
87
  if (!_themeId) {
52
88
  renderOnce(React.createElement(MissingThemeIdError, null));
53
89
  return;
54
90
  }
55
91
  const themeActionsApi = this.getApi(THEME_ACTIONS_API_NAME);
56
92
  const themeSkinstoreApi = this.getApi(THEME_SKINSTORE_API_NAME);
57
- const pushAction = themeActionsApi.getThemeAction({
58
- actionType: THEME_ACTIONS_TYPES.publishForm,
93
+ const publishAction = themeActionsApi.getThemeAction({
94
+ actionType: THEME_ACTIONS_TYPES.publish,
59
95
  themeId: _themeId,
60
96
  credentials
61
97
  });
62
- const controls = await themeSkinstoreApi.getPublishFormData({
63
- actionData: pushAction.data,
98
+ if (!publishAction) {
99
+ renderOnce(React.createElement(UnpermittedCommandError, { themeId: _themeId, commandName: "publish" }));
100
+ return;
101
+ }
102
+ const publishFormAction = themeActionsApi.getThemeAction({
103
+ actionType: THEME_ACTIONS_TYPES.publishForm,
104
+ themeId: _themeId,
64
105
  credentials
65
106
  });
66
- Form({
67
- controls,
68
- onSubmit: (formData) => {
69
- console.log('formData', formData);
70
- }
71
- });
107
+ const releaseType = this.flags.release === 'test' ? RELEASE_TYPE_TEST : RELEASE_TYPE_STABLE;
108
+ const isUpgrade = !publishFormAction;
109
+ if (isUpgrade) {
110
+ await this.#handleUpgrade({
111
+ themeSkinstoreApi,
112
+ themeActionsApi,
113
+ publishAction,
114
+ credentials,
115
+ releaseType,
116
+ themeId: _themeId,
117
+ loggerApi
118
+ });
119
+ }
120
+ else {
121
+ await this.#handleFirstPublish({
122
+ themeSkinstoreApi,
123
+ themeActionsApi,
124
+ publishFormAction,
125
+ publishAction,
126
+ credentials,
127
+ releaseType,
128
+ themeId: _themeId,
129
+ loggerApi
130
+ });
131
+ }
72
132
  }
73
133
  catch (err) {
134
+ if (err?.name === 'ExitPromptError') {
135
+ renderOnce(React.createElement(Info, { header: "Theme publish was cancelled." }));
136
+ return;
137
+ }
74
138
  loggerApi.error('Theme publish command error:', {
75
139
  error: err
76
140
  });
77
141
  renderOnce(React.createElement(ThemeError, { err: err, executionContext: executionContext }));
78
142
  }
79
143
  }
144
+ async #handleUpgrade({ themeSkinstoreApi, themeActionsApi, publishAction, credentials, releaseType, themeId, loggerApi }) {
145
+ renderOnce(React.createElement(Info, { header: `Upgrading published theme (ID: ${themeId})` },
146
+ React.createElement(Text, null,
147
+ "Release type: ",
148
+ this.flags.release)));
149
+ const { proceed } = await promptConfirmation('Proceed with upgrade?');
150
+ if (!proceed) {
151
+ renderOnce(React.createElement(Info, { header: "Theme publish was cancelled." }));
152
+ return;
153
+ }
154
+ const result = await themeSkinstoreApi.publishTheme({
155
+ actionData: publishAction.data,
156
+ credentials,
157
+ payload: { release: releaseType }
158
+ });
159
+ if (result?.isSuccess) {
160
+ themeActionsApi.removeThemeActions({ themeId, credentials });
161
+ renderOnce(React.createElement(Success, { header: "Theme upgraded successfully!" },
162
+ React.createElement(Text, null,
163
+ "Release type: ",
164
+ this.flags.release)));
165
+ }
166
+ else {
167
+ this.#renderPublishErrors(result?.messages);
168
+ }
169
+ }
170
+ async #handleFirstPublish({ themeSkinstoreApi, themeActionsApi, publishFormAction, publishAction, credentials, releaseType, themeId, loggerApi }) {
171
+ const controls = await themeSkinstoreApi.getPublishFormData({
172
+ actionData: publishFormAction.data,
173
+ credentials
174
+ });
175
+ if (!controls || controls.length === 0) {
176
+ renderOnce(React.createElement(ErrorBox, { header: "Could not fetch publish form." },
177
+ React.createElement(Text, null, "Check if SkinStore SDK feature is enabled for your store.")));
178
+ return;
179
+ }
180
+ const inquirerControls = toInquirerControls(controls.filter((c) => c.name !== 'release'));
181
+ const formData = await new Promise((resolve, reject) => {
182
+ Form({
183
+ controls: inquirerControls,
184
+ onSubmit: resolve
185
+ }).catch(reject);
186
+ });
187
+ let uploadedImageIds = [];
188
+ if (this.flags.images) {
189
+ uploadedImageIds = await this.#uploadImages({
190
+ themeSkinstoreApi,
191
+ credentials,
192
+ imagesDir: this.flags.images,
193
+ skinId: themeId,
194
+ loggerApi
195
+ });
196
+ }
197
+ const payload = {
198
+ name: formData.name,
199
+ desc: formData.desc ?? '',
200
+ description: formData.description,
201
+ categories: Array.isArray(formData.categories) ? formData.categories : [formData.categories],
202
+ colors: Array.isArray(formData.colors) ? formData.colors : [formData.colors],
203
+ price: Number(formData.price) || 0,
204
+ release: releaseType,
205
+ ...(uploadedImageIds.length > 0 ? { filesList: uploadedImageIds } : {})
206
+ };
207
+ const result = await themeSkinstoreApi.publishTheme({
208
+ actionData: publishAction.data,
209
+ credentials,
210
+ payload
211
+ });
212
+ if (result?.isSuccess) {
213
+ themeActionsApi.removeThemeActions({ themeId, credentials });
214
+ renderOnce(React.createElement(Success, { header: "Theme published successfully!" },
215
+ React.createElement(Text, null, "Your theme is now available on SkinStore.")));
216
+ }
217
+ else {
218
+ this.#renderPublishErrors(result?.messages);
219
+ }
220
+ }
221
+ async #uploadImages({ themeSkinstoreApi, credentials, imagesDir, skinId, loggerApi }) {
222
+ const imageIds = [];
223
+ if (!fs.existsSync(imagesDir)) {
224
+ renderOnce(React.createElement(ErrorBox, { header: "Images directory not found." },
225
+ React.createElement(Text, null,
226
+ "Path: ",
227
+ imagesDir)));
228
+ return imageIds;
229
+ }
230
+ const files = fs.readdirSync(imagesDir).filter((file) => {
231
+ const ext = path.extname(file).toLowerCase().replace('.', '');
232
+ return IMAGE_ALLOWED_EXTENSIONS.includes(ext);
233
+ });
234
+ for (const file of files) {
235
+ const filePath = path.join(imagesDir, file);
236
+ const stat = fs.statSync(filePath);
237
+ if (stat.size > IMAGE_MAX_SIZE) {
238
+ loggerApi.warn(`Skipping ${file}: exceeds ${IMAGE_MAX_SIZE} bytes limit`);
239
+ continue;
240
+ }
241
+ const uploadUrl = `/webapi/cli/themes/${skinId}/skinstore/files`;
242
+ const result = await themeSkinstoreApi.uploadImage({
243
+ credentials,
244
+ uploadUrl,
245
+ imagePath: filePath
246
+ });
247
+ if (result?.isSuccess && result?.imageId) {
248
+ imageIds.push(result.imageId);
249
+ }
250
+ else {
251
+ loggerApi.warn(`Failed to upload ${file}`, { details: result?.messages });
252
+ }
253
+ }
254
+ return imageIds;
255
+ }
256
+ #renderPublishErrors(messages) {
257
+ if (!messages) {
258
+ renderOnce(React.createElement(ErrorBox, { header: "Publishing failed." },
259
+ React.createElement(Text, null, "An unknown error occurred.")));
260
+ return;
261
+ }
262
+ renderOnce(React.createElement(ErrorBox, { header: "Publishing failed. Validation errors:" }, Object.entries(messages).map(([field, msg]) => (React.createElement(Text, { key: field },
263
+ field,
264
+ ": ",
265
+ typeof msg === 'string' ? msg : Object.values(msg).join(', '))))));
266
+ }
80
267
  }
@@ -10,10 +10,10 @@ export class ThemeSkinstoreApi extends FeatureApi {
10
10
  async getPublishFormData(props) {
11
11
  return this.#service.getPublishFormData(props);
12
12
  }
13
- async publish(themeId) {
14
- // return this.#service.publish(themeId);
13
+ async publishTheme(props) {
14
+ return this.#service.publishTheme(props);
15
15
  }
16
- async update() {
17
- // return this.#service.update();
16
+ async uploadImage(props) {
17
+ return this.#service.uploadImage(props);
18
18
  }
19
19
  }
@@ -14,4 +14,30 @@ export class ThemeSkinstoreHttpApi {
14
14
  isPrivate: true
15
15
  });
16
16
  }
17
+ publishTheme({ actionData, shopUrl, payload }) {
18
+ const { method, url } = actionData;
19
+ return this.#httpApi.fetch({
20
+ url: `${shopUrl}${url}`,
21
+ method,
22
+ data: payload,
23
+ sanitizeOptions: {
24
+ disable: true
25
+ },
26
+ isPrivate: true
27
+ });
28
+ }
29
+ uploadImage({ shopUrl, url, imageBuffer }) {
30
+ return this.#httpApi.fetch({
31
+ url: `${shopUrl}${url}`,
32
+ method: 'post',
33
+ data: imageBuffer,
34
+ headers: {
35
+ 'Content-Type': 'application/octet-stream'
36
+ },
37
+ sanitizeOptions: {
38
+ disable: true
39
+ },
40
+ isPrivate: true
41
+ });
42
+ }
17
43
  }
@@ -2,13 +2,18 @@ import { STATUS_CODES } from '@dreamcommerce/star_core';
2
2
  import { HttpErrorsFactory } from '../../../../../cli/class/errors/http/http_errors_factory.js';
3
3
  import { DownloadFileErrorsFactory } from '../../../../../utils/download_file/download_file_errors_factory.js';
4
4
  import { toControls } from '../../../../../cli/features/controls/controls_dto_mappers.js';
5
+ import fs from 'fs';
6
+ const PUBLISH_RESPONSE_STATUSES = [STATUS_CODES.ok, STATUS_CODES.badRequest];
5
7
  export class ThemeSkinstoreService {
6
8
  #httpApi;
7
- constructor(httpApi) {
9
+ #logger;
10
+ constructor({ httpApi, logger }) {
8
11
  this.#httpApi = httpApi;
12
+ this.#logger = logger;
9
13
  }
10
14
  async getPublishFormData({ credentials, actionData }) {
11
15
  try {
16
+ this.#logger.info('Fetching publish form data');
12
17
  const { response: request } = this.#httpApi.getPublishFormData({ actionData, shopUrl: credentials.shopUrl });
13
18
  const response = await request;
14
19
  if (response?.status !== STATUS_CODES.ok)
@@ -16,17 +21,64 @@ export class ThemeSkinstoreService {
16
21
  return response?.data ? toControls(response.data) : [];
17
22
  }
18
23
  catch (err) {
19
- //TODO to basic class
20
- switch (err.response?.status) {
21
- case 403:
22
- throw HttpErrorsFactory.createForbiddenError();
23
- case 401:
24
- throw HttpErrorsFactory.createUnauthorizedError();
25
- case 404:
26
- throw HttpErrorsFactory.createNotFoundError();
27
- default:
28
- throw DownloadFileErrorsFactory.downloadError(err.response.status);
29
- }
24
+ this.#logger.error('Error fetching publish form data', { error: err });
25
+ this.#handleHttpError(err);
26
+ }
27
+ }
28
+ async publishTheme({ credentials, actionData, payload }) {
29
+ try {
30
+ this.#logger.info('Publishing theme', {
31
+ details: { actionData }
32
+ });
33
+ const { response: request } = this.#httpApi.publishTheme({
34
+ actionData,
35
+ shopUrl: credentials.shopUrl,
36
+ payload
37
+ });
38
+ const response = await request;
39
+ if (!PUBLISH_RESPONSE_STATUSES.includes(response?.status))
40
+ return;
41
+ this.#logger.info('Successfully published theme');
42
+ return response?.data;
43
+ }
44
+ catch (err) {
45
+ this.#logger.error('Error publishing theme', { error: err });
46
+ this.#handleHttpError(err);
47
+ }
48
+ }
49
+ async uploadImage({ credentials, uploadUrl, imagePath }) {
50
+ try {
51
+ this.#logger.info('Uploading image', {
52
+ details: { uploadUrl, imagePath }
53
+ });
54
+ const imageBuffer = fs.readFileSync(imagePath);
55
+ const { response: request } = this.#httpApi.uploadImage({
56
+ shopUrl: credentials.shopUrl,
57
+ url: uploadUrl,
58
+ imageBuffer
59
+ });
60
+ const response = await request;
61
+ if (response?.status !== STATUS_CODES.ok)
62
+ return;
63
+ this.#logger.info('Successfully uploaded image');
64
+ return response?.data;
65
+ }
66
+ catch (err) {
67
+ this.#logger.error('Error uploading image', { error: err });
68
+ this.#handleHttpError(err);
69
+ }
70
+ }
71
+ #handleHttpError(err) {
72
+ //TODO to basic class
73
+ switch (err.response?.status) {
74
+ case 403:
75
+ throw HttpErrorsFactory.createForbiddenError();
76
+ case 401:
77
+ throw HttpErrorsFactory.createUnauthorizedError();
78
+ case 404:
79
+ throw HttpErrorsFactory.createNotFoundError();
80
+ default:
81
+ throw DownloadFileErrorsFactory.downloadError(err.response?.status);
30
82
  }
31
83
  }
32
84
  }
@@ -2,3 +2,7 @@ export const THEME_SKINSTORE_LOCATION = 'skinstore';
2
2
  export const THEME_SKINSTORE_SETTINGS_FILE_NAME = 'settings.json';
3
3
  export const THEME_SKINSTORE_API_NAME = 'ThemeSkinstoreApi';
4
4
  export const THEME_SKINSTORE_FEATURE_NAME = 'ThemeSkinstore';
5
+ export const RELEASE_TYPE_TEST = 0;
6
+ export const RELEASE_TYPE_STABLE = 1;
7
+ export const IMAGE_MAX_SIZE = 5242880;
8
+ export const IMAGE_ALLOWED_EXTENSIONS = ['gif', 'jpeg', 'jpg', 'png', 'webp'];
@@ -3,11 +3,16 @@ import { THEME_SKINSTORE_FEATURE_NAME } from './theme_publish_constants.js';
3
3
  import { ThemeSkinstoreService } from './service/theme_skinstore_service.js';
4
4
  import { ThemeSkinstoreHttpApi } from './http/theme_skinstore_http_api.js';
5
5
  import { ThemeSkinstoreApi } from './api/theme_skinstore_api.js';
6
+ import { LOGGER_API_NAME } from '../../../../cli/utilities/features/logger/logger_constants.js';
6
7
  export class ThemeSkinstoreInitializer extends SyncFeatureInitializer {
7
8
  static featureName = THEME_SKINSTORE_FEATURE_NAME;
8
9
  init() {
9
10
  const httpApi = this.getApiSync(HTTP_REQUESTER_API_NAME);
10
- const service = new ThemeSkinstoreService(new ThemeSkinstoreHttpApi(httpApi));
11
+ const loggerApi = this.getApiSync(LOGGER_API_NAME);
12
+ const service = new ThemeSkinstoreService({
13
+ httpApi: new ThemeSkinstoreHttpApi(httpApi),
14
+ logger: loggerApi
15
+ });
11
16
  return {
12
17
  cores: [
13
18
  {
@@ -114,6 +114,15 @@ export class ThemeFilesUtils {
114
114
  `${SHOPER_THEME_METADATA_DIR}/${THEME_CURRENT_CHECKSUMS_FILE_NAME}`,
115
115
  `${SHOPER_THEME_METADATA_DIR}/${THEME_CURRENT_CHECKSUMS_VERITY_FILE_NAME}`
116
116
  ];
117
+ if (!fileStructure[`${SHOPER_THEME_METADATA_DIR}/`]) {
118
+ fileStructure[`${SHOPER_THEME_METADATA_DIR}/`] = {
119
+ permissions: {
120
+ canAdd: true,
121
+ canEdit: true,
122
+ canDelete: false
123
+ }
124
+ };
125
+ }
117
126
  checksumsFiles.forEach((file) => {
118
127
  fileStructure[file] = {
119
128
  permissions: {
@@ -24,6 +24,8 @@ export class HiddenDirectoryUtils {
24
24
  const fileRelativePath = join(SHOPER_THEME_METADATA_DIR, fileName);
25
25
  const currentChecksum = await computeFileChecksum(fileFullPath);
26
26
  const initialChecksum = await themeChecksums.getInitialChecksumFromPath(fileRelativePath);
27
+ if (!initialChecksum)
28
+ continue;
27
29
  if (currentChecksum !== initialChecksum) {
28
30
  throw new Error(`File ${fileName} inside theme metadata directory (${themeMetadataPath}) has been modified. You cannot modify files inside .shoper directory`);
29
31
  }
@@ -4,5 +4,6 @@ export const THEME_COMMANDS_THAT_REQUIRED_ACTIONS_LIST = [
4
4
  THEME_COMMANDS_NAME.init,
5
5
  THEME_COMMANDS_NAME.push,
6
6
  THEME_COMMANDS_NAME.delete,
7
+ THEME_COMMANDS_NAME.publish,
7
8
  THEME_COMMANDS_NAME.watch
8
9
  ];
@@ -17,6 +17,7 @@ import { ThemeDeleteInitializer } from './features/theme/delete/theme_delete_ini
17
17
  import { ThemeActionsInitializer } from './features/theme/actions/theme_actions_initializer.js';
18
18
  import { ThemeWatchCommand } from './commands/watch/theme_watch_command.js';
19
19
  import { ThemeWatchInitializer } from './features/theme/watch/theme_watch_initializer.js';
20
+ import { ThemeSkinstoreInitializer } from './features/theme/skinstore/theme_skinstore_initialzier.js';
20
21
  export const COMMANDS = {
21
22
  [THEME_COMMANDS_NAME.list]: ThemeListCommand,
22
23
  [THEME_COMMANDS_NAME.pull]: ThemePullCommand,
@@ -34,6 +35,7 @@ export const COMMAND_TO_FEATURES_MAP = {
34
35
  [THEME_COMMANDS_NAME.push]: [ThemeFetchInitializer, ThemePushInitializer],
35
36
  [THEME_COMMANDS_NAME.verify]: ThemeVerifyInitializer,
36
37
  [THEME_COMMANDS_NAME.delete]: ThemeDeleteInitializer,
38
+ [THEME_COMMANDS_NAME.publish]: ThemeSkinstoreInitializer,
37
39
  [THEME_COMMANDS_NAME.watch]: [ThemeFetchInitializer, ThemePushInitializer, ThemeWatchInitializer]
38
40
  };
39
41
  export const getThemeInitializersForCommand = (commandName) => {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@shoper/cli",
3
3
  "packageManager": "yarn@3.2.0",
4
4
  "sideEffects": false,
5
- "version": "0.9.4-7",
5
+ "version": "0.9.6-0",
6
6
  "description": "CLI tool for Shoper",
7
7
  "author": "Joanna Firek",
8
8
  "license": "MIT",