@shoper/cli 0.1.0-20 → 0.1.0-22

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 (34) hide show
  1. package/build/cli/commands/auth/cli_auth_commands_utils.js +41 -39
  2. package/build/cli/commands/auth/cli_auth_list_tokens_command.js +2 -2
  3. package/build/cli/features/execution_context/execution_context_service.js +5 -5
  4. package/build/theme/commands/info/theme_info_command.js +97 -0
  5. package/build/theme/commands/info/theme_info_command_utils.js +39 -0
  6. package/build/theme/commands/init/theme_init_command.js +4 -4
  7. package/build/theme/commands/list/theme_list_command.js +25 -0
  8. package/build/theme/commands/list/theme_list_command_utils.js +27 -0
  9. package/build/theme/commands/pull/theme_pull_command.js +30 -4
  10. package/build/theme/commands/push/theme_push_command.js +32 -17
  11. package/build/theme/commands/theme_commands_constants.js +2 -1
  12. package/build/theme/commands/theme_verify_command.js +3 -3
  13. package/build/theme/commands/ui/invalid_theme_id.js +11 -0
  14. package/build/theme/commands/ui/theme_work_url_mismatch.js +17 -0
  15. package/build/theme/features/theme/actions/theme_actions_constants.js +2 -1
  16. package/build/theme/features/theme/fetch/service/theme_fetch_service.js +12 -3
  17. package/build/theme/features/theme/info/theme_info_utils.js +1 -0
  18. package/build/theme/features/theme/init/service/theme_init_service.js +2 -2
  19. package/build/theme/features/theme/push/theme_push_utils.js +2 -2
  20. package/build/theme/features/theme/utils/checksums/theme_checksums_utils.js +5 -5
  21. package/build/theme/features/theme/utils/files_structure/theme_files_structure_utils.js +34 -0
  22. package/build/theme/features/theme/utils/hidden_directory/hidden_directory_utils.js +33 -0
  23. package/build/theme/features/theme/utils/meta_data/theme_meta_data_constants.js +1 -0
  24. package/build/theme/features/theme/utils/meta_data/theme_meta_data_error_factory.js +15 -0
  25. package/build/theme/features/theme/utils/meta_data/theme_meta_data_utils.js +34 -0
  26. package/build/theme/features/themes/list/api/themes_list_api.js +3 -0
  27. package/build/theme/features/themes/list/services/themes_list_service.js +10 -3
  28. package/build/theme/hooks/ensure_theme_meta_data_untouched_hook.js +2 -2
  29. package/build/theme/hooks/themes_actions/ensure_themes_actions_hook.js +1 -0
  30. package/build/theme/index.js +4 -2
  31. package/package.json +1 -1
  32. package/build/theme/commands/theme_list_command.js +0 -15
  33. package/build/theme/features/theme/meta_data_directory/theme_directory_utils.js +0 -71
  34. /package/build/theme/features/theme/utils/{directories → resources}/theme_resources_with_id_directory_utils.js +0 -0
@@ -3,42 +3,44 @@ import { TOKEN_SCOPE_TO_DESCRIPTION } from './cli_auth_commands_constants.js';
3
3
  import { CSS_BACKGROUND_COLORS, CSS_TEXT_COLORS } from '../../../ui/color_constants.js';
4
4
  import { DateUtils } from '@dreamcommerce/utilities';
5
5
  import React from 'react';
6
- export const mapToTableData = (data, defaultIndex, tokensApi) => {
7
- return {
8
- headers: [
9
- {
10
- content: 'Index',
11
- width: 10
12
- },
13
- {
14
- content: 'Token Name',
15
- width: 40
16
- },
17
- {
18
- content: 'Store',
19
- width: 40
20
- },
21
- {
22
- content: 'Access Scope',
23
- width: 20
24
- },
25
- {
26
- content: 'Valid until',
27
- width: 20
28
- }
29
- ],
30
- rows: data.map((tokenData, index) => {
31
- const tokenIndex = index + 1;
32
- const isDefault = defaultIndex !== null && tokenIndex === defaultIndex;
33
- const tokenName = isDefault ? React.createElement(Text, { bold: true }, `${tokenData.name} (Default)`) : tokenData.name;
34
- const hasExpired = tokensApi.hasTokenExpired(tokenIndex);
35
- return [
36
- React.createElement(Text, { bold: isDefault }, tokenIndex),
37
- tokenName,
38
- React.createElement(Text, { bold: isDefault }, tokenData.iss),
39
- TOKEN_SCOPE_TO_DESCRIPTION[tokenData.scope],
40
- React.createElement(Text, { color: hasExpired ? CSS_TEXT_COLORS.invert : undefined, backgroundColor: hasExpired ? CSS_BACKGROUND_COLORS.danger : undefined }, DateUtils.toLocaleDate(new Date(tokenData.exp * 1000), 'pl-PL'))
41
- ];
42
- })
43
- };
44
- };
6
+ export class CliAuthCommandsUtils {
7
+ static mapToTableData(data, defaultIndex, tokensApi) {
8
+ return {
9
+ headers: [
10
+ {
11
+ content: 'Index',
12
+ width: 10
13
+ },
14
+ {
15
+ content: 'Token Name',
16
+ width: 40
17
+ },
18
+ {
19
+ content: 'Store',
20
+ width: 40
21
+ },
22
+ {
23
+ content: 'Access Scope',
24
+ width: 20
25
+ },
26
+ {
27
+ content: 'Valid until',
28
+ width: 20
29
+ }
30
+ ],
31
+ rows: data.map((tokenData, index) => {
32
+ const tokenIndex = index + 1;
33
+ const isDefault = defaultIndex !== null && tokenIndex === defaultIndex;
34
+ const tokenName = isDefault ? React.createElement(Text, { bold: true }, `${tokenData.name} (Default)`) : tokenData.name;
35
+ const hasExpired = tokensApi.hasTokenExpired(tokenIndex);
36
+ return [
37
+ React.createElement(Text, { bold: isDefault }, tokenIndex),
38
+ tokenName,
39
+ React.createElement(Text, { bold: isDefault }, tokenData.iss),
40
+ TOKEN_SCOPE_TO_DESCRIPTION[tokenData.scope],
41
+ React.createElement(Text, { color: hasExpired ? CSS_TEXT_COLORS.invert : undefined, backgroundColor: hasExpired ? CSS_BACKGROUND_COLORS.danger : undefined }, DateUtils.toLocaleDate(new Date(tokenData.exp * 1000), 'pl-PL'))
42
+ ];
43
+ })
44
+ };
45
+ }
46
+ }
@@ -1,7 +1,7 @@
1
1
  import { BaseCliCommand } from '../../class/base_cli_command.js';
2
2
  import { CLI_AUTH_TOKENS_API_NAME } from '../../auth/tokens/cli_auth_tokens_constants.js';
3
3
  import React from 'react';
4
- import { mapToTableData } from './cli_auth_commands_utils.js';
4
+ import { CliAuthCommandsUtils } from './cli_auth_commands_utils.js';
5
5
  import { Table } from '../../../ui/table/table.js';
6
6
  import { render } from '../../../ui/ui_utils.js';
7
7
  import { Box } from '../../../ui/box.js';
@@ -25,6 +25,6 @@ export class CliAuthListTokensCommand extends BaseCliCommand {
25
25
  React.createElement(Command, null, "shoper auth add-token"))));
26
26
  return;
27
27
  }
28
- await render(React.createElement(Table, { data: mapToTableData(tokensPayloads, tokensApi.getDefaultTokenIndex(), tokensApi) }));
28
+ await render(React.createElement(Table, { data: CliAuthCommandsUtils.mapToTableData(tokensPayloads, tokensApi.getDefaultTokenIndex(), tokensApi) }));
29
29
  }
30
30
  }
@@ -1,10 +1,10 @@
1
1
  import process from 'node:process';
2
2
  import { EXECUTION_CONTEXTS } from './execution_context_constants.js';
3
- import { ThemeDirectoryUtils } from '../../../theme/features/theme/meta_data_directory/theme_directory_utils.js';
3
+ import { ThemeMetaDataUtils } from '../../../theme/features/theme/utils/meta_data/theme_meta_data_utils.js';
4
4
  export class ExecutionContextService {
5
5
  #executionContext;
6
6
  async _obtainExecutionContext() {
7
- if (await ThemeDirectoryUtils.closestThemeRootDirectory(process.cwd())) {
7
+ if (await ThemeMetaDataUtils.closestThemeRootDirectory(process.cwd())) {
8
8
  return await this.getThemeExecutionContext();
9
9
  }
10
10
  return await this._getGlobalExecutionContext();
@@ -20,14 +20,14 @@ export class ExecutionContextService {
20
20
  return this.#executionContext;
21
21
  }
22
22
  async getThemeExecutionContext() {
23
- const themeRoot = await ThemeDirectoryUtils.closestThemeRootDirectory(process.cwd());
23
+ const themeRoot = await ThemeMetaDataUtils.closestThemeRootDirectory(process.cwd());
24
24
  //TODO errors
25
25
  if (!themeRoot)
26
26
  throw 'Theme root directory not found';
27
- const themeDirectoryMetadata = await ThemeDirectoryUtils.getThemeDirectoryMetadata(themeRoot);
27
+ const themeMetaData = await ThemeMetaDataUtils.getThemeMetadata(themeRoot);
28
28
  return {
29
29
  type: EXECUTION_CONTEXTS.theme,
30
- themeId: themeDirectoryMetadata.themeId,
30
+ themeId: themeMetaData.themeId,
31
31
  executionDir: process.cwd(),
32
32
  themeRootDir: themeRoot
33
33
  };
@@ -0,0 +1,97 @@
1
+ import { BaseThemeCommand } from '../../class/base_theme_command.js';
2
+ import { CLI_AUTH_API_NAME } from '../../../cli/auth/cli_auth_constants.js';
3
+ import { EXECUTION_CONTEXT_API_NAME, EXECUTION_CONTEXTS } from '../../../cli/features/execution_context/execution_context_constants.js';
4
+ import { renderOnce } from '../../../ui/ui_utils.js';
5
+ import { MissingCredentialsError } from '../../../cli/commands/auth/ui/missing_credentials_error.js';
6
+ import React from 'react';
7
+ import { THEMES_LIST_API_NAME } from '../../features/themes/list/themes_list_constants.js';
8
+ import { Args } from '@oclif/core';
9
+ import { ThemeMetaDataUtils } from '../../features/theme/utils/meta_data/theme_meta_data_utils.js';
10
+ import { MissingThemeIdError } from '../ui/missing_theme_id_error.js';
11
+ import { Text } from '../../../ui/text.js';
12
+ import { THEME_WORK_URL_MISMATCH_ERROR } from '../../features/theme/utils/meta_data/theme_meta_data_constants.js';
13
+ import { Error } from '../../../ui/message_box/error.js';
14
+ import { ThemeWorkUrlMismatch } from '../ui/theme_work_url_mismatch.js';
15
+ import { Table } from '../../../ui/table/table.js';
16
+ import { ThemeInfoCommandUtils } from './theme_info_command_utils.js';
17
+ import { InvalidThemeIdError } from '../ui/invalid_theme_id.js';
18
+ import { Box } from '../../../ui/box.js';
19
+ export class ThemeInfoCommand extends BaseThemeCommand {
20
+ static summary = 'View details about the current theme.';
21
+ static description = 'Displays key information about the theme you’re working on — including its ID, name, description (if provided), and status.\n\nYou can run this command from a specific theme directory (ID not needed) or outside any theme directory (theme ID required).';
22
+ static examples = [
23
+ {
24
+ description: 'The command will display the theme ID, name, description, and status.',
25
+ command: '<%= config.bin %> <%= command.id %> 2'
26
+ }
27
+ ];
28
+ static args = {
29
+ id: Args.string({
30
+ description: 'Theme id',
31
+ name: 'id',
32
+ type: 'string'
33
+ })
34
+ };
35
+ async run() {
36
+ const data = await this.parse(ThemeInfoCommand);
37
+ const { args } = data;
38
+ const themeId = args.id;
39
+ const cliAuthApi = this.getApi(CLI_AUTH_API_NAME);
40
+ const executionContextApi = this.getApi(EXECUTION_CONTEXT_API_NAME);
41
+ const themesListApi = this.getApi(THEMES_LIST_API_NAME);
42
+ const credentials = cliAuthApi.getCredentials();
43
+ if (!credentials) {
44
+ renderOnce(React.createElement(MissingCredentialsError, null));
45
+ return;
46
+ }
47
+ const executionContext = await executionContextApi.getExecutionContext();
48
+ let theme;
49
+ let _themeId = themeId;
50
+ try {
51
+ if (executionContext.type === EXECUTION_CONTEXTS.theme) {
52
+ await ThemeMetaDataUtils.ensureThemeWorkUrlMatch(executionContext.themeRootDir, credentials.shopUrl);
53
+ _themeId = themeId ?? executionContext.themeId;
54
+ theme = await themesListApi.getTheme({
55
+ themeId: _themeId,
56
+ shopUrl: credentials.shopUrl
57
+ });
58
+ }
59
+ if (executionContext.type !== EXECUTION_CONTEXTS.theme) {
60
+ if (_themeId === undefined) {
61
+ renderOnce(React.createElement(MissingThemeIdError, null,
62
+ React.createElement(Box, { flexDirection: "column", gap: 1 },
63
+ React.createElement(Text, null, "Usage: shoper theme info [ID]"),
64
+ React.createElement(Text, null, "Please run this command inside a theme directory or provide a theme ID."))));
65
+ return;
66
+ }
67
+ theme = await themesListApi.getTheme({
68
+ themeId: _themeId,
69
+ shopUrl: credentials.shopUrl
70
+ });
71
+ }
72
+ if (!theme) {
73
+ renderOnce(_themeId ? React.createElement(InvalidThemeIdError, { themeId: _themeId }) : React.createElement(Error, { header: "Error: Theme information is not available." }));
74
+ return;
75
+ }
76
+ this._displayThemeInfo(theme);
77
+ }
78
+ catch (err) {
79
+ this._handleError(err);
80
+ }
81
+ }
82
+ _handleError(err) {
83
+ if (err?.code === THEME_WORK_URL_MISMATCH_ERROR) {
84
+ renderOnce(React.createElement(ThemeWorkUrlMismatch, { command: "info" }));
85
+ return;
86
+ }
87
+ if (err?.message) {
88
+ renderOnce(React.createElement(Error, null,
89
+ React.createElement(Text, null, err.message)));
90
+ return;
91
+ }
92
+ this.error(String(err));
93
+ }
94
+ _displayThemeInfo(theme) {
95
+ renderOnce(React.createElement(Table, { data: ThemeInfoCommandUtils.mapThemeMetaDataToTableData(theme) }));
96
+ }
97
+ }
@@ -0,0 +1,39 @@
1
+ import { Text } from '../../../ui/text.js';
2
+ import React from 'react';
3
+ export class ThemeInfoCommandUtils {
4
+ static mapThemeMetaDataToTableData(theme) {
5
+ return {
6
+ headers: [
7
+ {
8
+ content: 'Theme Info'
9
+ }
10
+ ],
11
+ rows: [
12
+ [
13
+ React.createElement(Text, null,
14
+ React.createElement(Text, { bold: true }, "ID:"),
15
+ " ",
16
+ theme.skinId)
17
+ ],
18
+ [
19
+ React.createElement(Text, null,
20
+ React.createElement(Text, { bold: true }, "Name:"),
21
+ " ",
22
+ theme.title)
23
+ ],
24
+ [
25
+ React.createElement(Text, null,
26
+ React.createElement(Text, { bold: true }, "Description:"),
27
+ " ",
28
+ theme.description ? theme.description : '-')
29
+ ],
30
+ [
31
+ React.createElement(Text, null,
32
+ React.createElement(Text, { bold: true }, "Status: "),
33
+ ": $",
34
+ theme.isActive ? 'Active' : 'Inactive')
35
+ ]
36
+ ]
37
+ };
38
+ }
39
+ }
@@ -13,7 +13,7 @@ import { UnpermittedCommandError } from '../ui/unpermitted_command_error.js';
13
13
  import { MissingCredentialsError } from '../../../cli/commands/auth/ui/missing_credentials_error.js';
14
14
  import { ThemeCreatedSuccess } from './ui/theme_created_success.js';
15
15
  import { Text } from '../../../ui/text.js';
16
- import { Newline } from 'ink';
16
+ import { Box } from '../../../ui/box.js';
17
17
  export class ThemeInitCommand extends BaseThemeCommand {
18
18
  static summary = 'Creates a copy of an existing theme by duplicating it.';
19
19
  static description = 'The new theme will be named after the source theme with a "Copy" suffix and an incremental number.\n\nYou must run this command from the root directory (not inside any theme folder) and provide the ID of the theme you want to duplicate.';
@@ -37,9 +37,9 @@ export class ThemeInitCommand extends BaseThemeCommand {
37
37
  const themeId = args.id;
38
38
  if (!themeId) {
39
39
  renderOnce(React.createElement(MissingThemeIdError, null,
40
- React.createElement(Text, null, "Usage: shoper theme init [ID]"),
41
- React.createElement(Newline, null),
42
- React.createElement(Text, null, "This command creates a copy of an existing theme based on its ID.To proceed, you must provide the ID of the theme you want to duplicate.")));
40
+ React.createElement(Box, { flexDirection: "column", gap: 1 },
41
+ React.createElement(Text, null, "Usage: shoper theme init [ID]"),
42
+ React.createElement(Text, null, "This command creates a copy of an existing theme based on its ID.To proceed, you must provide the ID of the theme you want to duplicate."))));
43
43
  return;
44
44
  }
45
45
  const cliAuthApi = this.getApi(CLI_AUTH_API_NAME);
@@ -0,0 +1,25 @@
1
+ import { BaseThemeCommand } from '../../class/base_theme_command.js';
2
+ import { THEMES_LIST_API_NAME } from '../../features/themes/list/themes_list_constants.js';
3
+ import { CLI_AUTH_API_NAME } from '../../../cli/auth/cli_auth_constants.js';
4
+ import { renderOnce } from '../../../ui/ui_utils.js';
5
+ import { Info } from '../../../ui/message_box/info.js';
6
+ import React from 'react';
7
+ import { Table } from '../../../ui/table/table.js';
8
+ import { ThemeListCommandUtils } from './theme_list_command_utils.js';
9
+ export class ThemeListCommand extends BaseThemeCommand {
10
+ static summary = 'Displays a list of all themes available in your store, including their IDs, names, and statuses (active/inactive).';
11
+ static description = 'Use this to check which theme is now active in your store.';
12
+ async run() {
13
+ const ThemesListApi = this.getApi(THEMES_LIST_API_NAME);
14
+ const cliAuthApi = this.getApi(CLI_AUTH_API_NAME);
15
+ const credentials = cliAuthApi.getCredentials();
16
+ if (!credentials)
17
+ this.error('Credentials not found. Please authorize first.');
18
+ const themes = await ThemesListApi.getThemes(credentials);
19
+ if (!themes?.length) {
20
+ renderOnce(React.createElement(Info, { header: "No themes found for this store." }));
21
+ return;
22
+ }
23
+ renderOnce(React.createElement(Table, { data: ThemeListCommandUtils.mapThemeListToTableData(themes) }));
24
+ }
25
+ }
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import { Text } from '../../../ui/text.js';
3
+ export class ThemeListCommandUtils {
4
+ static mapThemeListToTableData(themes) {
5
+ return {
6
+ headers: [
7
+ {
8
+ content: 'Theme ID',
9
+ width: 10
10
+ },
11
+ {
12
+ content: 'Theme Name',
13
+ width: 70
14
+ },
15
+ {
16
+ content: 'Status',
17
+ width: 10
18
+ }
19
+ ],
20
+ rows: themes.map((theme, index) => [
21
+ React.createElement(Text, null, index),
22
+ React.createElement(Text, null, theme.title),
23
+ React.createElement(Text, null, theme.isActive ? 'active' : 'inactive')
24
+ ])
25
+ };
26
+ }
27
+ }
@@ -9,7 +9,7 @@ import tmp from 'tmp-promise';
9
9
  import { join } from '../../../utils/path_utils.js';
10
10
  import { THEME_ACTION_NOT_FOUND_ERROR_CODE, THEME_ACTIONS_API_NAME, THEME_ACTIONS_TYPES } from '../../features/theme/actions/theme_actions_constants.js';
11
11
  import process from 'node:process';
12
- import { ThemeResourcesWithIdDirectoryUtils } from '../../features/theme/utils/directories/theme_resources_with_id_directory_utils.js';
12
+ import { ThemeResourcesWithIdDirectoryUtils } from '../../features/theme/utils/resources/theme_resources_with_id_directory_utils.js';
13
13
  import { renderOnce } from '../../../ui/ui_utils.js';
14
14
  import { MissingCredentialsError } from '../../../cli/commands/auth/ui/missing_credentials_error.js';
15
15
  import React from 'react';
@@ -22,10 +22,24 @@ import { promptConfirmation } from '../../../ui/prompts/prompt_confirmation.js';
22
22
  import { ThemePulledSuccess } from './ui/theme_pulled_success.js';
23
23
  import { Info } from '../../../ui/message_box/info.js';
24
24
  import { directoryExists } from '../../../utils/fs/fs_utils.js';
25
+ import { THEME_WORK_URL_MISMATCH_ERROR } from '../../features/theme/utils/meta_data/theme_meta_data_constants.js';
26
+ import { ThemeWorkUrlMismatch } from '../ui/theme_work_url_mismatch.js';
27
+ import { Error } from '../../../ui/message_box/error.js';
28
+ import { ThemeMetaDataUtils } from '../../features/theme/utils/meta_data/theme_meta_data_utils.js';
25
29
  //TODO jak jest error w pullu wowczas usuwamy docelowo pociagniety skin
26
30
  export class ThemePullCommand extends BaseThemeCommand {
27
- static description = 'Pull theme from shop';
28
- #spinner;
31
+ static summary = 'Downloads the current version of your theme from the store and overwrites your local theme files.';
32
+ static description = 'Make sure to back up any local modifications you wish to keep.\n\nYou can run this command from a specific theme directory (ID not needed) or outside any theme directory (theme ID required).\n\nThis keeps your local theme the same as the version in your store.';
33
+ static examples = [
34
+ {
35
+ description: 'This will update your local theme files with the current version from the store.',
36
+ command: '<%= config.bin %> <%= command.id %>'
37
+ },
38
+ {
39
+ description: 'This will download the theme with ID "2" from the store.',
40
+ command: '<%= config.bin %> <%= command.id %> 2'
41
+ }
42
+ ];
29
43
  static args = {
30
44
  id: Args.string({
31
45
  description: 'Theme id',
@@ -40,6 +54,7 @@ export class ThemePullCommand extends BaseThemeCommand {
40
54
  options: Object.values(THEME_FETCH_TYPES)
41
55
  })
42
56
  };
57
+ #spinner;
43
58
  async run() {
44
59
  const data = await this.parse(ThemePullCommand);
45
60
  const { args, flags } = data;
@@ -88,6 +103,15 @@ export class ThemePullCommand extends BaseThemeCommand {
88
103
  this._renderUnpermittedCommandError(_themeId);
89
104
  return;
90
105
  }
106
+ if (err?.code === THEME_WORK_URL_MISMATCH_ERROR) {
107
+ renderOnce(React.createElement(ThemeWorkUrlMismatch, { command: "pull" }));
108
+ return;
109
+ }
110
+ if (err?.message) {
111
+ renderOnce(React.createElement(Error, null,
112
+ React.createElement(Text, null, err.message)));
113
+ return;
114
+ }
91
115
  this.error(String(err));
92
116
  }
93
117
  _renderUnpermittedCommandError(themeId) {
@@ -101,7 +125,7 @@ export class ThemePullCommand extends BaseThemeCommand {
101
125
  }
102
126
  const pullAction = themeActionsApi.getThemeAction({
103
127
  actionType: THEME_ACTIONS_TYPES.pull,
104
- themeId: themeId,
128
+ themeId,
105
129
  credentials
106
130
  });
107
131
  if (!pullAction) {
@@ -121,6 +145,8 @@ export class ThemePullCommand extends BaseThemeCommand {
121
145
  renderOnce(React.createElement(ThemePulledSuccess, null));
122
146
  }
123
147
  async _pullThemeIntoExistingOne({ themeId, themeFetchApi, themeActionsApi, fetchType, executionContext, credentials }) {
148
+ //TODO move these logis somvewhere else?
149
+ await ThemeMetaDataUtils.ensureThemeWorkUrlMatch(executionContext.themeRootDir, credentials.shopUrl);
124
150
  const pullAction = themeActionsApi.getThemeAction({
125
151
  actionType: THEME_ACTIONS_TYPES.pull,
126
152
  themeId,
@@ -3,7 +3,7 @@ import { CLI_AUTH_API_NAME } from '../../../cli/auth/cli_auth_constants.js';
3
3
  import { EXECUTION_CONTEXT_API_NAME, EXECUTION_CONTEXTS } from '../../../cli/features/execution_context/execution_context_constants.js';
4
4
  import { THEME_ACTION_NOT_FOUND_ERROR_CODE, THEME_ACTIONS_API_NAME, THEME_ACTIONS_TYPES } from '../../features/theme/actions/theme_actions_constants.js';
5
5
  import { THEME_ARCHIVE_UPLOAD_ERROR, THEME_PUSH_API_NAME } from '../../features/theme/push/theme_push_constants.js';
6
- import { ThemeDirectoryUtils } from '../../features/theme/meta_data_directory/theme_directory_utils.js';
6
+ import { ThemeMetaDataUtils } from '../../features/theme/utils/meta_data/theme_meta_data_utils.js';
7
7
  import { mapChecksumToTree } from '../../../utils/checksums/checksums_utils.js';
8
8
  import { mapToPermissionsTree } from '../../utils/directory_validator/directory_validator_utils.js';
9
9
  import { ThemeChecksumsUtils } from '../../features/theme/utils/checksums/theme_checksums_utils.js';
@@ -23,6 +23,9 @@ import { ThemeUnpermittedActionsError } from './ui/theme_unpermitted_actions_err
23
23
  import { ThemeValidationErrors } from './ui/theme_validation_errors.js';
24
24
  import { Error } from '../../../ui/message_box/error.js';
25
25
  import { Text } from '../../../ui/text.js';
26
+ import { ThemeWorkUrlMismatch } from '../ui/theme_work_url_mismatch.js';
27
+ import { THEME_WORK_URL_MISMATCH_ERROR } from '../../features/theme/utils/meta_data/theme_meta_data_constants.js';
28
+ import { ThemeFilesStructureUtils } from '../../features/theme/utils/files_structure/theme_files_structure_utils.js';
26
29
  import { ThemeInfoUtils } from '../../features/theme/info/theme_info_utils.js';
27
30
  export class ThemePushCommand extends BaseThemeCommand {
28
31
  static summary = 'Uploads your local theme files to the store and overwrites the current version of the theme in your store.';
@@ -50,6 +53,7 @@ export class ThemePushCommand extends BaseThemeCommand {
50
53
  }
51
54
  let spinner;
52
55
  try {
56
+ await ThemeMetaDataUtils.ensureThemeWorkUrlMatch(executionContext.themeRootDir, credentials.shopUrl);
53
57
  const pushAction = themeActionsApi.getThemeAction({
54
58
  actionType: THEME_ACTIONS_TYPES.push,
55
59
  themeId: executionContext.themeId,
@@ -60,7 +64,7 @@ export class ThemePushCommand extends BaseThemeCommand {
60
64
  return;
61
65
  }
62
66
  const checksums = await ThemeChecksumsUtils.getThemeInitialChecksums(executionContext.themeRootDir);
63
- const permissions = await ThemeDirectoryUtils.getFilesPermissions(executionContext.themeRootDir);
67
+ const permissions = await ThemeFilesStructureUtils.getFilesPermissions(executionContext.themeRootDir);
64
68
  if (!checksums || !permissions)
65
69
  this.error('Theme checksums or permissions not found. Please ensure you are in the correct theme directory.');
66
70
  const themeMergeApi = this.getApi(THEME_MERGE_API_NAME);
@@ -69,7 +73,7 @@ export class ThemePushCommand extends BaseThemeCommand {
69
73
  renderOnce(React.createElement(ThemePushSkipInfo, null));
70
74
  return;
71
75
  }
72
- const validationResult = await ThemeDirectoryUtils.validateThemeDirectoryStructure({
76
+ const validationResult = await ThemeFilesStructureUtils.validateThemeDirectoryStructure({
73
77
  checksums: mapChecksumToTree(checksums),
74
78
  permissions: mapToPermissionsTree(permissions),
75
79
  rootDirectory: executionContext.themeRootDir
@@ -80,7 +84,7 @@ export class ThemePushCommand extends BaseThemeCommand {
80
84
  renderOnce(React.createElement(ThemeUnpermittedActionsError, { unpermittedActions: validationResult.unpermittedActions }));
81
85
  return;
82
86
  }
83
- const filesStructure = await ThemeDirectoryUtils.getThemeFilesStructure(executionContext.themeRootDir);
87
+ const filesStructure = await ThemeFilesStructureUtils.getThemeFilesStructure(executionContext.themeRootDir);
84
88
  if (!filesStructure) {
85
89
  renderOnce(React.createElement(MissingThemeFiles, { files: `${SHOPER_THEME_METADATA_DIR}/${THEME_FILES_STRUCTURE_FILE_NAME}` }));
86
90
  return;
@@ -98,19 +102,30 @@ export class ThemePushCommand extends BaseThemeCommand {
98
102
  }
99
103
  catch (err) {
100
104
  spinner?.stop();
101
- if (err?.code === THEME_ACTION_NOT_FOUND_ERROR_CODE) {
102
- renderOnce(React.createElement(UnpermittedCommandError, { themeId: executionContext.themeId, commandName: "push" }));
103
- return;
104
- }
105
- if (err?.code === THEME_ARCHIVE_UPLOAD_ERROR) {
106
- renderOnce(React.createElement(ThemeValidationErrors, { errors: err?.details }));
107
- return;
108
- }
109
- if (err?.message) {
110
- renderOnce(React.createElement(Error, null,
111
- React.createElement(Text, null, err.message)));
112
- return;
113
- }
105
+ this._handleError(err, executionContext);
114
106
  }
115
107
  }
108
+ _handleError(err, executionContext) {
109
+ if (err?.code === THEME_ACTION_NOT_FOUND_ERROR_CODE) {
110
+ renderOnce(React.createElement(UnpermittedCommandError, { themeId: executionContext.themeId, commandName: "push" }));
111
+ return;
112
+ }
113
+ if (err?.code === THEME_ARCHIVE_UPLOAD_ERROR) {
114
+ renderOnce(React.createElement(ThemeValidationErrors, { errors: err?.details }));
115
+ return;
116
+ }
117
+ if (err?.code === THEME_WORK_URL_MISMATCH_ERROR) {
118
+ this._renderUrlMismatchError();
119
+ return;
120
+ }
121
+ if (err?.message) {
122
+ renderOnce(React.createElement(Error, null,
123
+ React.createElement(Text, null, err.message)));
124
+ return;
125
+ }
126
+ this.error(String(err));
127
+ }
128
+ _renderUrlMismatchError() {
129
+ renderOnce(React.createElement(ThemeWorkUrlMismatch, { command: "push" }));
130
+ }
116
131
  }
@@ -4,5 +4,6 @@ export const THEME_COMMANDS_NAME = {
4
4
  init: 'theme:init',
5
5
  push: 'theme:push',
6
6
  showChanges: 'theme:show-changes',
7
- verify: 'theme:verify'
7
+ verify: 'theme:verify',
8
+ info: 'theme:info'
8
9
  };
@@ -1,10 +1,10 @@
1
1
  import { BaseThemeCommand } from '../class/base_theme_command.js';
2
2
  import { CLI_AUTH_API_NAME } from '../../cli/auth/cli_auth_constants.js';
3
3
  import { EXECUTION_CONTEXT_API_NAME, EXECUTION_CONTEXTS } from '../../cli/features/execution_context/execution_context_constants.js';
4
- import { ThemeDirectoryUtils } from '../features/theme/meta_data_directory/theme_directory_utils.js';
5
4
  import { mapToPermissionsTree } from '../utils/directory_validator/directory_validator_utils.js';
6
5
  import { mapChecksumToTree } from '../../utils/checksums/checksums_utils.js';
7
6
  import { ThemeChecksumsUtils } from '../features/theme/utils/checksums/theme_checksums_utils.js';
7
+ import { ThemeFilesStructureUtils } from '../features/theme/utils/files_structure/theme_files_structure_utils.js';
8
8
  export class ThemeVerifyCommand extends BaseThemeCommand {
9
9
  static description = 'Verify theme files structure';
10
10
  static hidden = true;
@@ -18,11 +18,11 @@ export class ThemeVerifyCommand extends BaseThemeCommand {
18
18
  if (executionContext.type !== EXECUTION_CONTEXTS.theme)
19
19
  this.error('You cannot run this command outside theme context.');
20
20
  const checksums = await ThemeChecksumsUtils.getThemeInitialChecksums(executionContext.themeRootDir);
21
- const permissions = await ThemeDirectoryUtils.getFilesPermissions(executionContext.themeRootDir);
21
+ const permissions = await ThemeFilesStructureUtils.getFilesPermissions(executionContext.themeRootDir);
22
22
  if (!checksums || !permissions)
23
23
  this.error('Theme checksums or permissions not found. Please ensure you are in the correct theme directory.');
24
24
  try {
25
- const validationResult = await ThemeDirectoryUtils.validateThemeDirectoryStructure({
25
+ const validationResult = await ThemeFilesStructureUtils.validateThemeDirectoryStructure({
26
26
  checksums: mapChecksumToTree(checksums),
27
27
  permissions: mapToPermissionsTree(permissions),
28
28
  rootDirectory: executionContext.themeRootDir
@@ -0,0 +1,11 @@
1
+ import { Error } from '../../../ui/message_box/error.js';
2
+ import { Tip } from '../../../ui/tip.js';
3
+ import { Command } from '../../../ui/command.js';
4
+ import React from 'react';
5
+ export const InvalidThemeIdError = ({ themeId }) => {
6
+ return (React.createElement(Error, { header: `Error: Invalid theme ID: ${themeId}` },
7
+ React.createElement(Tip, null,
8
+ "Run ",
9
+ React.createElement(Command, null, "shoper theme list"),
10
+ " to see available theme IDs.")));
11
+ };
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { Error } from '../../../ui/message_box/error.js';
3
+ import { Command } from '../../../ui/command.js';
4
+ import { Tip } from '../../../ui/tip.js';
5
+ import { Text } from '../../../ui/text.js';
6
+ export const ThemeWorkUrlMismatch = ({ command }) => {
7
+ return (React.createElement(Error, { header: `Error: This theme belongs to a different store.` },
8
+ React.createElement(Text, null,
9
+ "Switch to the correct token to ",
10
+ command,
11
+ " this theme."),
12
+ React.createElement(Tip, null,
13
+ React.createElement(Text, null,
14
+ "Run ",
15
+ React.createElement(Command, null, "shoper theme auth token switch"),
16
+ " to change your token"))));
17
+ };
@@ -7,7 +7,8 @@ export const THEME_ACTIONS_TYPES = {
7
7
  pull: 'pull',
8
8
  delete: 'delete',
9
9
  publish_form: 'publish_form',
10
- publish: 'publish'
10
+ publish: 'publish',
11
+ info: 'info'
11
12
  };
12
13
  export const THEME_ACTION_DATA_TYPE = {
13
14
  file: 'file',
@@ -7,7 +7,6 @@ import { SHOPER_THEME_METADATA_DIR } from '../../../../constants/directory_conts
7
7
  import { getResources, mapResourcesToTree } from '../../../../class/fetch_resources/fetch_resources_utils.js';
8
8
  import { FetchResources } from '../../../../class/fetch_resources/fetch_resources.js';
9
9
  import { jsonIndentTransform } from '../../../../../utils/stream_transforms/json_indent_transform.js';
10
- import { ThemeDirectoryUtils } from '../../meta_data_directory/theme_directory_utils.js';
11
10
  import { THEME_CURRENT_CHECKSUMS_FILE_NAME, THEME_CURRENT_CHECKSUMS_VERITY_FILE_NAME, THEME_INITIAL_CHECKSUMS_FILE_NAME, THEME_INITIAL_CHECKSUMS_VERITY_FILE_NAME } from '../../theme_constants.js';
12
11
  import { THEME_FILES_LIST_FILE_NAME } from '../../push/theme_push_constants.js';
13
12
  import { JSON_FILE_INDENT } from '../../../../../cli/cli_constants.js';
@@ -15,6 +14,8 @@ import { ThemeChecksumsUtils } from '../../utils/checksums/theme_checksums_utils
15
14
  import { createWriteStream } from 'node:fs';
16
15
  import { AppError } from '../../../../../cli/class/errors/app/app_error.js';
17
16
  import { THEME_ACTIONS_TYPES } from '../../actions/theme_actions_constants.js';
17
+ import { ThemeFilesStructureUtils } from '../../utils/files_structure/theme_files_structure_utils.js';
18
+ import { ThemeMetaDataUtils } from '../../utils/meta_data/theme_meta_data_utils.js';
18
19
  export class ThemeFetchService {
19
20
  #themeHttpApi;
20
21
  #httpApi;
@@ -53,6 +54,7 @@ export class ThemeFetchService {
53
54
  });
54
55
  }
55
56
  await this._createGitIgnoreFile(themeDir);
57
+ await this._updateMetadataFileWithWorkUrl(themeDir, credentials.shopUrl);
56
58
  const checksums = await ThemeChecksumsUtils.computeThemeChecksums(themeDir);
57
59
  await ThemeChecksumsUtils.createThemeChecksumsFiles(themeDir, checksums);
58
60
  return {
@@ -60,7 +62,7 @@ export class ThemeFetchService {
60
62
  };
61
63
  }
62
64
  async _updateFilesStructure(themeDir) {
63
- const fileStructure = await ThemeDirectoryUtils.getThemeFilesStructure(themeDir);
65
+ const fileStructure = await ThemeFilesStructureUtils.getThemeFilesStructure(themeDir);
64
66
  const checksumsFiles = [
65
67
  `${SHOPER_THEME_METADATA_DIR}/${THEME_INITIAL_CHECKSUMS_FILE_NAME}`,
66
68
  `${SHOPER_THEME_METADATA_DIR}/${THEME_INITIAL_CHECKSUMS_VERITY_FILE_NAME}`,
@@ -93,7 +95,14 @@ export class ThemeFetchService {
93
95
  }
94
96
  };
95
97
  });
96
- await ThemeDirectoryUtils.writeThemeFilesStructure(themeDir, fileStructure);
98
+ await ThemeFilesStructureUtils.writeThemeFilesStructure(themeDir, fileStructure);
99
+ }
100
+ async _updateMetadataFileWithWorkUrl(themeDir, workUrl) {
101
+ const shopMetadata = await ThemeMetaDataUtils.getThemeMetadata(themeDir);
102
+ await ThemeMetaDataUtils.updateThemeMetadata(themeDir, {
103
+ ...shopMetadata,
104
+ workUrl
105
+ });
97
106
  }
98
107
  async fetchResources(shopUrl, dist, resources) {
99
108
  const resourcesTree = mapResourcesToTree(resources);
@@ -2,6 +2,7 @@ import { readJSONFile } from '../../../../utils/fs/fs_utils.js';
2
2
  import { join } from '../../../../utils/path_utils.js';
3
3
  export class ThemeInfoUtils {
4
4
  static async getThemeName(themeDir) {
5
+ //TODO - wziasc z themeList a to wywalic
5
6
  const themeDetails = await readJSONFile(join(themeDir, 'settings', 'details.json'));
6
7
  return themeDetails.name ?? 'Storefront';
7
8
  }
@@ -4,7 +4,7 @@ import tmp from 'tmp-promise';
4
4
  import { downloadFile } from '../../../../../utils/download_file/download_file_utils.js';
5
5
  import { extractZip } from '../../../../../utils/zip/extract_zip_utils.js';
6
6
  import { ThemeChecksumsUtils } from '../../utils/checksums/theme_checksums_utils.js';
7
- import { ThemeDirectoryUtils } from '../../meta_data_directory/theme_directory_utils.js';
7
+ import { ThemeMetaDataUtils } from '../../utils/meta_data/theme_meta_data_utils.js';
8
8
  import { ThemeInfoUtils } from '../../info/theme_info_utils.js';
9
9
  export class ThemeInitService {
10
10
  #httpApi;
@@ -25,7 +25,7 @@ export class ThemeInitService {
25
25
  });
26
26
  const checksums = await ThemeChecksumsUtils.computeThemeChecksums(distDir);
27
27
  await ThemeChecksumsUtils.createThemeChecksumsFiles(join(process.cwd(), basename), checksums);
28
- const themeMetaData = await ThemeDirectoryUtils.getThemeDirectoryMetadata(distDir);
28
+ const themeMetaData = await ThemeMetaDataUtils.getThemeMetadata(distDir);
29
29
  return {
30
30
  themeId: themeMetaData.themeId,
31
31
  themeName: await ThemeInfoUtils.getThemeName(distDir)
@@ -1,10 +1,10 @@
1
1
  import globs from 'fast-glob';
2
- import { ThemeDirectoryUtils } from '../meta_data_directory/theme_directory_utils.js';
3
2
  import { AppError } from '../../../../cli/class/errors/app/app_error.js';
4
3
  import { toUnixPath } from '../../../../utils/path_utils.js';
4
+ import { ThemeFilesStructureUtils } from '../utils/files_structure/theme_files_structure_utils.js';
5
5
  export class ThemePushUtils {
6
6
  static async getAllFilesThatAreSendToRemote(themeDir) {
7
- const filesStructure = await ThemeDirectoryUtils.getThemeFilesStructure(themeDir);
7
+ const filesStructure = await ThemeFilesStructureUtils.getThemeFilesStructure(themeDir);
8
8
  if (!filesStructure)
9
9
  throw new AppError({
10
10
  message: `Files structure not found in theme directory: ${themeDir}`,
@@ -3,25 +3,25 @@ import { THEME_CURRENT_CHECKSUMS_FILE_NAME, THEME_CURRENT_CHECKSUMS_VERITY_FILE_
3
3
  import { createWriteStream } from 'fs';
4
4
  import { copyFile, readJSONFile, removeFile } from '../../../../../utils/fs/fs_utils.js';
5
5
  import { ThemeChecksumsErrorFactory } from './theme_checksums_error_factory.js';
6
- import { ThemeDirectoryUtils } from '../../meta_data_directory/theme_directory_utils.js';
7
6
  import { computeChecksumsFromFilesStructure, computeDirectoriesChecksums, computeFileChecksum } from '../../../../../utils/checksums/checksums_utils.js';
8
7
  import { JSON_FILE_INDENT } from '../../../../../cli/cli_constants.js';
9
8
  import { ThemePushUtils } from '../../push/theme_push_utils.js';
10
9
  import { SHOPER_THEME_METADATA_DIR } from '../../../../constants/directory_contstants.js';
11
10
  import { isWindowsOs } from '../../../../../utils/platform_utils.js';
12
11
  import { normalize } from 'path';
12
+ import { HiddenDirectoryUtils } from '../hidden_directory/hidden_directory_utils.js';
13
13
  export class ThemeChecksumsUtils {
14
14
  static getCurrentThemeChecksumsFilePath(themeDir) {
15
- return join(ThemeDirectoryUtils.getThemeMetaDataDirPath(themeDir), THEME_CURRENT_CHECKSUMS_FILE_NAME);
15
+ return join(HiddenDirectoryUtils.getThemeHiddenDirectoryPath(themeDir), THEME_CURRENT_CHECKSUMS_FILE_NAME);
16
16
  }
17
17
  static getInitialThemeChecksumsFilePath(themeDir) {
18
- return join(ThemeDirectoryUtils.getThemeMetaDataDirPath(themeDir), THEME_INITIAL_CHECKSUMS_FILE_NAME);
18
+ return join(HiddenDirectoryUtils.getThemeHiddenDirectoryPath(themeDir), THEME_INITIAL_CHECKSUMS_FILE_NAME);
19
19
  }
20
20
  static getCurrentThemeChecksumsVerificationFilePath(themeDir) {
21
- return join(ThemeDirectoryUtils.getThemeMetaDataDirPath(themeDir), THEME_CURRENT_CHECKSUMS_VERITY_FILE_NAME);
21
+ return join(HiddenDirectoryUtils.getThemeHiddenDirectoryPath(themeDir), THEME_CURRENT_CHECKSUMS_VERITY_FILE_NAME);
22
22
  }
23
23
  static getInitialThemeChecksumsVerificationFilePath(themeDir) {
24
- return join(ThemeDirectoryUtils.getThemeMetaDataDirPath(themeDir), THEME_INITIAL_CHECKSUMS_VERITY_FILE_NAME);
24
+ return join(HiddenDirectoryUtils.getThemeHiddenDirectoryPath(themeDir), THEME_INITIAL_CHECKSUMS_VERITY_FILE_NAME);
25
25
  }
26
26
  static async createThemeChecksumsFiles(themeDir, checksums) {
27
27
  return new Promise((resolve, reject) => {
@@ -0,0 +1,34 @@
1
+ import { readJSONFile, writeJSONFile } from '../../../../../utils/fs/fs_utils.js';
2
+ import { join, mapKeysPathsToWindowPlatform } from '../../../../../utils/path_utils.js';
3
+ import { SHOPER_THEME_METADATA_DIR } from '../../../../constants/directory_contstants.js';
4
+ import { THEME_FILES_STRUCTURE_FILE_NAME } from '../../theme_constants.js';
5
+ import { isWindowsOs } from '../../../../../utils/platform_utils.js';
6
+ import { validateDirectory } from '../../../../utils/directory_validator/directory_validator_utils.js';
7
+ export class ThemeFilesStructureUtils {
8
+ static async getThemeFilesStructure(themeDirectory) {
9
+ const filesStructure = await readJSONFile(join(themeDirectory, SHOPER_THEME_METADATA_DIR, THEME_FILES_STRUCTURE_FILE_NAME));
10
+ if (!isWindowsOs())
11
+ return filesStructure;
12
+ return mapKeysPathsToWindowPlatform(filesStructure);
13
+ }
14
+ static async writeThemeFilesStructure(themeDirectory, filesStructure) {
15
+ const filePath = join(themeDirectory, SHOPER_THEME_METADATA_DIR, THEME_FILES_STRUCTURE_FILE_NAME);
16
+ await writeJSONFile(filePath, filesStructure);
17
+ }
18
+ static async getFilesPermissions(themeDirectory) {
19
+ const fileStructure = await ThemeFilesStructureUtils.getThemeFilesStructure(themeDirectory);
20
+ return Object.entries(fileStructure).reduce((acc, [key, value]) => {
21
+ return {
22
+ ...acc,
23
+ [key]: value.permissions
24
+ };
25
+ }, {});
26
+ }
27
+ static async validateThemeDirectoryStructure({ checksums, permissions, rootDirectory }) {
28
+ return await validateDirectory({
29
+ permissions,
30
+ checksums,
31
+ rootDirectory
32
+ });
33
+ }
34
+ }
@@ -0,0 +1,33 @@
1
+ import { ThemeChecksumsUtils } from '../checksums/theme_checksums_utils.js';
2
+ import { getAllFilesNamesInside } from '../../../../../utils/fs/fs_utils.js';
3
+ import { THEME_CURRENT_CHECKSUMS_FILE_NAME, THEME_CURRENT_CHECKSUMS_VERITY_FILE_NAME, THEME_INITIAL_CHECKSUMS_FILE_NAME, THEME_INITIAL_CHECKSUMS_VERITY_FILE_NAME } from '../../theme_constants.js';
4
+ import { join } from '../../../../../utils/path_utils.js';
5
+ import { SHOPER_THEME_METADATA_DIR } from '../../../../constants/directory_contstants.js';
6
+ import { computeFileChecksum } from '../../../../../utils/checksums/checksums_utils.js';
7
+ export class HiddenDirectoryUtils {
8
+ static async ensureFilesInsideThemeMetaDataDirectoryUntouched(themeDirectory) {
9
+ const themeMetadataPath = this.getThemeHiddenDirectoryPath(themeDirectory);
10
+ const checksums = await ThemeChecksumsUtils.getThemeCurrentChecksums(themeDirectory);
11
+ if (!checksums)
12
+ throw new Error(`Checksums file not found in theme metadata directory: ${themeMetadataPath}`);
13
+ if (!(await ThemeChecksumsUtils.verifyThemeChecksums(themeDirectory)))
14
+ throw new Error('Theme checksum file is not valid');
15
+ const filesNames = (await getAllFilesNamesInside(themeMetadataPath)).filter((fileName) => fileName !== THEME_CURRENT_CHECKSUMS_FILE_NAME &&
16
+ fileName !== THEME_CURRENT_CHECKSUMS_VERITY_FILE_NAME &&
17
+ fileName !== THEME_INITIAL_CHECKSUMS_FILE_NAME &&
18
+ fileName !== THEME_INITIAL_CHECKSUMS_VERITY_FILE_NAME);
19
+ for (const fileName of filesNames) {
20
+ if (fileName === THEME_CURRENT_CHECKSUMS_FILE_NAME)
21
+ continue;
22
+ const fileFullPath = join(themeMetadataPath, fileName);
23
+ const fileRelativePath = join(SHOPER_THEME_METADATA_DIR, fileName);
24
+ const currentChecksum = await computeFileChecksum(fileFullPath);
25
+ if (currentChecksum !== checksums[fileRelativePath]) {
26
+ throw new Error(`File ${fileName} inside theme metadata directory (${themeMetadataPath}) has been modified. You cannot modify files inside .shoper directory`);
27
+ }
28
+ }
29
+ }
30
+ static getThemeHiddenDirectoryPath(themeDirectory) {
31
+ return join(themeDirectory, SHOPER_THEME_METADATA_DIR);
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ export const THEME_WORK_URL_MISMATCH_ERROR = 'theme.meta_data.error_work_url_mismatch';
@@ -0,0 +1,15 @@
1
+ import { AppError } from '../../../../../cli/class/errors/app/app_error.js';
2
+ import { THEME_WORK_URL_MISMATCH_ERROR } from './theme_meta_data_constants.js';
3
+ export class ThemeMetaDataErrorFactory {
4
+ static createThemeWorkUrlMismatchError(executionUrl, themeWorkUrl) {
5
+ return new AppError({
6
+ code: THEME_WORK_URL_MISMATCH_ERROR,
7
+ message: `Theme work URL "${themeWorkUrl}" does not match execution URL "${executionUrl}".`,
8
+ level: 'error',
9
+ details: {
10
+ executionUrl,
11
+ themeWorkUrl
12
+ }
13
+ });
14
+ }
15
+ }
@@ -0,0 +1,34 @@
1
+ import { closestFile, readJSONFile, writeJSONFile } from '../../../../../utils/fs/fs_utils.js';
2
+ import { SHOPER_THEME_METADATA_DIR } from '../../../../constants/directory_contstants.js';
3
+ import { join } from '../../../../../utils/path_utils.js';
4
+ import { HiddenDirectoryUtils } from '../hidden_directory/hidden_directory_utils.js';
5
+ import { ThemeMetaDataErrorFactory } from './theme_meta_data_error_factory.js';
6
+ export class ThemeMetaDataUtils {
7
+ static async closestThemeRootDirectory(path) {
8
+ return closestFile(SHOPER_THEME_METADATA_DIR, path);
9
+ }
10
+ static getThemeMetadataFilePath(themeDirectory) {
11
+ return join(HiddenDirectoryUtils.getThemeHiddenDirectoryPath(themeDirectory), 'metadata.json');
12
+ }
13
+ static async getThemeMetadata(themeDirectory) {
14
+ const filePath = this.getThemeMetadataFilePath(themeDirectory);
15
+ const metadataFileContent = await readJSONFile(filePath);
16
+ return {
17
+ themeId: String(metadataFileContent.themeId)
18
+ };
19
+ }
20
+ static async updateThemeMetadata(themeDirectory, data) {
21
+ const filePath = this.getThemeMetadataFilePath(themeDirectory);
22
+ await writeJSONFile(filePath, data);
23
+ }
24
+ static async getThemeWorkUrl(themeDirectory) {
25
+ const metadataFilePath = this.getThemeMetadataFilePath(themeDirectory);
26
+ const metadata = await readJSONFile(metadataFilePath);
27
+ return metadata.workUrl || '';
28
+ }
29
+ static async ensureThemeWorkUrlMatch(themeDirectory, executionWorkUrl) {
30
+ const metadataWorkUrl = await this.getThemeWorkUrl(themeDirectory);
31
+ if (metadataWorkUrl !== executionWorkUrl)
32
+ throw ThemeMetaDataErrorFactory.createThemeWorkUrlMismatchError(executionWorkUrl, metadataWorkUrl);
33
+ }
34
+ }
@@ -10,4 +10,7 @@ export class ThemesListApi extends FeatureApi {
10
10
  async getThemes(credentials) {
11
11
  return this.#service.getThemes(credentials);
12
12
  }
13
+ async getTheme(props) {
14
+ return this.#service.getTheme(props);
15
+ }
13
16
  }
@@ -3,13 +3,13 @@ import { STATUS_CODES } from '@dreamcommerce/star_core';
3
3
  import { HttpErrorsFactory } from '../../../../../cli/class/errors/http/http_errors_factory.js';
4
4
  import { DownloadFileErrorsFactory } from '../../../../../utils/download_file/download_file_errors_factory.js';
5
5
  export class ThemesListService {
6
- httpApi;
6
+ #httpApi;
7
7
  constructor(httpApi) {
8
- this.httpApi = httpApi;
8
+ this.#httpApi = httpApi;
9
9
  }
10
10
  async getThemes({ shopUrl }) {
11
11
  try {
12
- const { response } = this.httpApi.getThemes(shopUrl);
12
+ const { response } = this.#httpApi.getThemes(shopUrl);
13
13
  const { data, status } = await response;
14
14
  if (status !== STATUS_CODES.ok)
15
15
  return;
@@ -20,6 +20,7 @@ export class ThemesListService {
20
20
  }));
21
21
  }
22
22
  catch (err) {
23
+ //TODO to basic class
23
24
  switch (err.response?.status) {
24
25
  case 403:
25
26
  throw HttpErrorsFactory.createForbiddenError();
@@ -34,4 +35,10 @@ export class ThemesListService {
34
35
  }
35
36
  }
36
37
  }
38
+ async getTheme({ themeId, shopUrl }) {
39
+ const themes = await this.getThemes({ shopUrl });
40
+ if (!themes)
41
+ return;
42
+ return themes.find((theme) => String(theme.skinId) === themeId);
43
+ }
37
44
  }
@@ -1,13 +1,13 @@
1
1
  import { useApi } from '../../cli/hooks/ensure_cli_initialized_hook.js';
2
2
  import { EXECUTION_CONTEXT_API_NAME, EXECUTION_CONTEXTS } from '../../cli/features/execution_context/execution_context_constants.js';
3
- import { ThemeDirectoryUtils } from '../features/theme/meta_data_directory/theme_directory_utils.js';
3
+ import { HiddenDirectoryUtils } from '../features/theme/utils/hidden_directory/hidden_directory_utils.js';
4
4
  const ensureThemeMetaDataUntouched = async () => {
5
5
  const executionContextApi = useApi(EXECUTION_CONTEXT_API_NAME);
6
6
  const executionContext = await executionContextApi.getExecutionContext();
7
7
  if (executionContext.type !== EXECUTION_CONTEXTS.theme)
8
8
  return;
9
9
  try {
10
- await ThemeDirectoryUtils.ensureFilesInsideThemeMetaDataDirectoryUntouched(executionContext.themeRootDir);
10
+ await HiddenDirectoryUtils.ensureFilesInsideThemeMetaDataDirectoryUntouched(executionContext.themeRootDir);
11
11
  }
12
12
  catch (err) {
13
13
  console.error(err);
@@ -31,6 +31,7 @@ const ensureThemesActionsHook = async ({ Command, argv }) => {
31
31
  console.error('Unauthorized. Please authorize first.');
32
32
  const cliAuthTokensApi = useApi(CLI_AUTH_TOKENS_API_NAME);
33
33
  await promptForToken(cliAuthTokensApi);
34
+ //TODO loader
34
35
  await themesActionsApi.ensureThemesActions({
35
36
  credentials,
36
37
  themeId
@@ -1,15 +1,17 @@
1
- import { ThemeListCommand } from './commands/theme_list_command.js';
1
+ import { ThemeListCommand } from './commands/list/theme_list_command.js';
2
2
  import { ThemePullCommand } from './commands/pull/theme_pull_command.js';
3
3
  import { ThemeInitCommand } from './commands/init/theme_init_command.js';
4
4
  import { THEME_COMMANDS_NAME } from './commands/theme_commands_constants.js';
5
5
  import { ThemePushCommand } from './commands/push/theme_push_command.js';
6
6
  import { ThemeShowChangesCommand } from './commands/theme_show_changes_command.js';
7
7
  import { ThemeVerifyCommand } from './commands/theme_verify_command.js';
8
+ import { ThemeInfoCommand } from './commands/info/theme_info_command.js';
8
9
  export const COMMANDS = {
9
10
  [THEME_COMMANDS_NAME.list]: ThemeListCommand,
10
11
  [THEME_COMMANDS_NAME.pull]: ThemePullCommand,
11
12
  [THEME_COMMANDS_NAME.init]: ThemeInitCommand,
12
13
  [THEME_COMMANDS_NAME.push]: ThemePushCommand,
13
14
  [THEME_COMMANDS_NAME.showChanges]: ThemeShowChangesCommand,
14
- [THEME_COMMANDS_NAME.verify]: ThemeVerifyCommand
15
+ [THEME_COMMANDS_NAME.verify]: ThemeVerifyCommand,
16
+ [THEME_COMMANDS_NAME.info]: ThemeInfoCommand
15
17
  };
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.1.0-20",
5
+ "version": "0.1.0-22",
6
6
  "description": "CLI tool for Shoper",
7
7
  "author": "Joanna Firek",
8
8
  "license": "MIT",
@@ -1,15 +0,0 @@
1
- import { BaseThemeCommand } from '../class/base_theme_command.js';
2
- import { THEMES_LIST_API_NAME } from '../features/themes/list/themes_list_constants.js';
3
- import { CLI_AUTH_API_NAME } from '../../cli/auth/cli_auth_constants.js';
4
- export class ThemeListCommand extends BaseThemeCommand {
5
- static description = 'List all shop themes';
6
- async run() {
7
- const ThemesListApi = this.getApi(THEMES_LIST_API_NAME);
8
- const cliAuthApi = this.getApi(CLI_AUTH_API_NAME);
9
- const credentials = cliAuthApi.getCredentials();
10
- if (!credentials)
11
- this.error('Credentials not found. Please authorize first.');
12
- const themes = await ThemesListApi.getThemes(credentials);
13
- this.log('themes', themes);
14
- }
15
- }
@@ -1,71 +0,0 @@
1
- import { closestFile, getAllFilesNamesInside, readJSONFile, writeJSONFile } from '../../../../utils/fs/fs_utils.js';
2
- import { SHOPER_THEME_METADATA_DIR } from '../../../constants/directory_contstants.js';
3
- import { join, mapKeysPathsToWindowPlatform } from '../../../../utils/path_utils.js';
4
- import { validateDirectory } from '../../../utils/directory_validator/directory_validator_utils.js';
5
- import { THEME_CURRENT_CHECKSUMS_FILE_NAME, THEME_CURRENT_CHECKSUMS_VERITY_FILE_NAME, THEME_FILES_STRUCTURE_FILE_NAME, THEME_INITIAL_CHECKSUMS_FILE_NAME, THEME_INITIAL_CHECKSUMS_VERITY_FILE_NAME } from '../theme_constants.js';
6
- import { ThemeChecksumsUtils } from '../utils/checksums/theme_checksums_utils.js';
7
- import { computeFileChecksum } from '../../../../utils/checksums/checksums_utils.js';
8
- import { isWindowsOs } from '../../../../utils/platform_utils.js';
9
- export class ThemeDirectoryUtils {
10
- static async closestThemeRootDirectory(path) {
11
- return closestFile(SHOPER_THEME_METADATA_DIR, path);
12
- }
13
- static getThemeMetaDataDirPath(themeDirectory) {
14
- return join(themeDirectory, SHOPER_THEME_METADATA_DIR);
15
- }
16
- static async getThemeDirectoryMetadata(themeDirectory) {
17
- const filePath = join(this.getThemeMetaDataDirPath(themeDirectory), 'metadata.json');
18
- const metadataFileContent = await readJSONFile(filePath);
19
- return {
20
- themeId: String(metadataFileContent.skin_id)
21
- };
22
- }
23
- static async getThemeFilesStructure(themeDirectory) {
24
- const filesStructure = await readJSONFile(join(themeDirectory, SHOPER_THEME_METADATA_DIR, THEME_FILES_STRUCTURE_FILE_NAME));
25
- if (!isWindowsOs())
26
- return filesStructure;
27
- return mapKeysPathsToWindowPlatform(filesStructure);
28
- }
29
- static async writeThemeFilesStructure(themeDirectory, filesStructure) {
30
- const filePath = join(themeDirectory, SHOPER_THEME_METADATA_DIR, THEME_FILES_STRUCTURE_FILE_NAME);
31
- await writeJSONFile(filePath, filesStructure);
32
- }
33
- static async getFilesPermissions(themeDirectory) {
34
- const fileStructure = await ThemeDirectoryUtils.getThemeFilesStructure(themeDirectory);
35
- return Object.entries(fileStructure).reduce((acc, [key, value]) => {
36
- return {
37
- ...acc,
38
- [key]: value.permissions
39
- };
40
- }, {});
41
- }
42
- static async validateThemeDirectoryStructure({ checksums, permissions, rootDirectory }) {
43
- return await validateDirectory({
44
- permissions,
45
- checksums,
46
- rootDirectory
47
- });
48
- }
49
- static async ensureFilesInsideThemeMetaDataDirectoryUntouched(themeDirectory) {
50
- const themeMetadataPath = this.getThemeMetaDataDirPath(themeDirectory);
51
- const checksums = await ThemeChecksumsUtils.getThemeCurrentChecksums(themeDirectory);
52
- if (!checksums)
53
- throw new Error(`Checksums file not found in theme metadata directory: ${themeMetadataPath}`);
54
- if (!(await ThemeChecksumsUtils.verifyThemeChecksums(themeDirectory)))
55
- throw new Error('Theme checksum file is not valid');
56
- const filesNames = (await getAllFilesNamesInside(themeMetadataPath)).filter((fileName) => fileName !== THEME_CURRENT_CHECKSUMS_FILE_NAME &&
57
- fileName !== THEME_CURRENT_CHECKSUMS_VERITY_FILE_NAME &&
58
- fileName !== THEME_INITIAL_CHECKSUMS_FILE_NAME &&
59
- fileName !== THEME_INITIAL_CHECKSUMS_VERITY_FILE_NAME);
60
- for (const fileName of filesNames) {
61
- if (fileName === THEME_CURRENT_CHECKSUMS_FILE_NAME)
62
- continue;
63
- const fileFullPath = join(themeMetadataPath, fileName);
64
- const fileRelativePath = join(SHOPER_THEME_METADATA_DIR, fileName);
65
- const currentChecksum = await computeFileChecksum(fileFullPath);
66
- if (currentChecksum !== checksums[fileRelativePath]) {
67
- throw new Error(`File ${fileName} inside theme metadata directory (${themeMetadataPath}) has been modified. You cannot modify files inside .shoper directory`);
68
- }
69
- }
70
- }
71
- }