@scandipwa/magento-scripts 2.4.0-alpha.1 → 2.4.0-alpha.2

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 (51) hide show
  1. package/lib/config/docker.js +164 -56
  2. package/lib/config/port-config.js +46 -10
  3. package/lib/config/services/elasticsearch/default-es-env.js +1 -1
  4. package/lib/config/services/mariadb/versions/mariadb-10.2.js +3 -1
  5. package/lib/config/services/mariadb/versions/mariadb-10.3.js +3 -1
  6. package/lib/config/services/mariadb/versions/mariadb-10.4.js +3 -1
  7. package/lib/config/services/mariadb/versions/mariadb-10.6.js +3 -1
  8. package/lib/config/services/mariadb/versions/mariadb-11.4.js +3 -1
  9. package/lib/config/services/mariadb/versions/mariadb-11.6.js +3 -1
  10. package/lib/config/services/opensearch/default-os-env.js +1 -1
  11. package/lib/config/services/php/extensions/xdebug.js +1 -0
  12. package/lib/config/templates/nginx.template.conf +2 -2
  13. package/lib/config/templates/php-fpm.template.conf +1 -1
  14. package/lib/config/templates/ssl-terminator.template.conf +1 -1
  15. package/lib/tasks/database/create-magento-database.js +2 -1
  16. package/lib/tasks/database/import-remote-db/ssh/index.js +1 -1
  17. package/lib/tasks/database/import-remote-db/ssh/readymage.js +1 -1
  18. package/lib/tasks/database/import-remote-db/ssh/regular-server.js +1 -1
  19. package/lib/tasks/docker/containers/container-api.d.ts +5 -0
  20. package/lib/tasks/docker/containers/container-api.js +3 -1
  21. package/lib/tasks/docker/containers/tasks.js +86 -21
  22. package/lib/tasks/docker/project-image-builder.js +55 -45
  23. package/lib/tasks/docker/system/system-api.d.ts +66 -0
  24. package/lib/tasks/docker/system/system-api.js +28 -1
  25. package/lib/tasks/execute.js +1 -1
  26. package/lib/tasks/file-system/create-nginx-config.js +22 -8
  27. package/lib/tasks/file-system/create-ssl-terminator-config.js +20 -7
  28. package/lib/tasks/magento/install-magento-project.js +40 -24
  29. package/lib/tasks/magento/setup-magento/check-file-permissions.php +32 -0
  30. package/lib/tasks/magento/setup-magento/index.js +2 -0
  31. package/lib/tasks/magento/setup-magento/make-magento-binaries-executable.js +44 -0
  32. package/lib/tasks/magento/setup-magento/setup-file-permissions.js +160 -0
  33. package/lib/tasks/php/php-container.js +20 -6
  34. package/lib/tasks/php/update-env-php.js +3 -9
  35. package/lib/tasks/requirements/cgroup-version.js +69 -0
  36. package/lib/tasks/requirements/elasticsearch-version.js +19 -3
  37. package/lib/tasks/requirements/index.js +3 -0
  38. package/lib/tasks/requirements/opensearch-version.js +1 -1
  39. package/lib/tasks/requirements/searchengine-version.js +1 -2
  40. package/lib/util/dockerfile-builder/build-instructions.js +5 -1
  41. package/lib/util/dockerfile-builder/types.d.ts +1 -1
  42. package/lib/util/execute-in-container.js +3 -1
  43. package/lib/util/get-installed-magento-version.js +60 -2
  44. package/lib/util/portscanner.js +3 -3
  45. package/lib/util/run-composer.js +1 -1
  46. package/lib/util/run-magento.js +2 -1
  47. package/lib/util/run-php.js +2 -1
  48. package/lib/util/set-config.js +4 -2
  49. package/package.json +16 -16
  50. package/typings/context.d.ts +4 -2
  51. package/typings/index.d.ts +10 -0
@@ -148,40 +148,105 @@ const pullImages = () => ({
148
148
  */
149
149
  const startContainers = () => ({
150
150
  title: 'Starting containers',
151
- task: async ({ ports, config: { docker }, debug }, task) => {
151
+ task: async ({ ports, config: { docker } }, task) => {
152
152
  const containerList = await containerApi.ls({
153
153
  formatToJSON: true,
154
154
  all: true
155
155
  })
156
156
 
157
- const missingContainers = Object.values(
158
- docker.getContainers(ports)
159
- ).filter(({ name }) => !containerList.some((c) => c.Names === name))
157
+ const missingContainers = Object.entries(docker.getContainers(ports))
158
+ .filter(
159
+ ([nameWithoutPrefix, { name }]) =>
160
+ !containerList.some((c) => c.Names === name)
161
+ )
162
+ .map(([nameWithoutPrefix, containerOptions]) => ({
163
+ ...containerOptions,
164
+ nameWithoutPrefix
165
+ }))
160
166
 
161
167
  if (missingContainers.length === 0) {
162
168
  task.skip()
163
169
  return
164
170
  }
165
171
 
166
- if (debug) {
167
- await Promise.all(
168
- missingContainers.map((container) =>
169
- containerApi.run(container).then((out) => {
170
- task.output = `From ${container._}: ${out}`
171
- })
172
- )
173
- )
172
+ const containerStatuses = missingContainers.reduce(
173
+ (acc, container) => ({
174
+ ...acc,
175
+ [container.nameWithoutPrefix]: {
176
+ started: false,
177
+ onStarted: []
178
+ }
179
+ }),
180
+ {}
181
+ )
174
182
 
175
- return
176
- }
183
+ return task.newListr(
184
+ missingContainers.map((container) => ({
185
+ title: `Deploying ${logger.style.file(container._)} container`,
186
+ task: async (subCtx, subTask) => {
187
+ const { dependsOn } = container
188
+ if (Array.isArray(dependsOn)) {
189
+ const startedContainers = []
190
+ subTask.title = `Container ${
191
+ container._
192
+ } is waiting for ${dependsOn.join(', ')} to start...`
193
+ await Promise.all(
194
+ dependsOn.map(
195
+ async (name) =>
196
+ new Promise((resolve, reject) => {
197
+ const timeout = setTimeout(
198
+ () => {
199
+ reject(
200
+ new Error(
201
+ `Container ${name} not started in time`
202
+ )
203
+ )
204
+ },
205
+ // 2 minutes
206
+ 1000 * 60 * 2
207
+ )
208
+ containerStatuses[name].onStarted.push(
209
+ () => {
210
+ startedContainers.push(name)
211
+ subTask.title = `Container ${
212
+ container._
213
+ } is waiting for ${dependsOn
214
+ .filter(
215
+ (d) =>
216
+ !startedContainers.includes(
217
+ d
218
+ )
219
+ )
220
+ .join(', ')} to start...`
221
+ clearTimeout(timeout)
222
+ resolve()
223
+ }
224
+ )
225
+ })
226
+ )
227
+ )
228
+ }
177
229
 
178
- // TODO: we might stop containers here ?
179
- await Promise.all(
180
- missingContainers.map((container) =>
181
- containerApi.run(container).then((out) => {
182
- task.output = `From ${container._}: ${out}`
183
- })
184
- )
230
+ subTask.title = `${container._} is starting...`
231
+
232
+ await containerApi.run(container)
233
+
234
+ containerStatuses[
235
+ container.nameWithoutPrefix
236
+ ].started = true
237
+ containerStatuses[
238
+ container.nameWithoutPrefix
239
+ ].onStarted.forEach((cb) => {
240
+ cb()
241
+ })
242
+
243
+ subTask.output = `${container._} container started`
244
+ }
245
+ })),
246
+ {
247
+ concurrent: true,
248
+ exitOnError: true
249
+ }
185
250
  )
186
251
  },
187
252
  options: {
@@ -170,9 +170,8 @@ const buildDockerFileInstructions = async (
170
170
  }
171
171
 
172
172
  if (!ctx.isDockerDesktop) {
173
- dockerFileInstructions.run(
174
- `chown -R ${os.userInfo().uid}:${os.userInfo().gid} /composer/home`
175
- )
173
+ const { uid, gid } = os.userInfo()
174
+ dockerFileInstructions.run(`chown -R ${uid}:${gid} /composer/home`)
176
175
  }
177
176
 
178
177
  dockerFileInstructions.workDir(ctx.config.baseConfig.containerMagentoDir)
@@ -286,58 +285,69 @@ const buildProjectImage = () => ({
286
285
  title: 'Building Project Images',
287
286
  task: async (ctx, task) => {
288
287
  const containers = ctx.config.docker.getContainers(ctx.ports)
289
- const [image, tag = 'latest'] =
290
- ctx.config.overridenConfiguration.configuration.php.baseImage.split(
291
- ':'
292
- )
293
- const dockerFileInstructions = await buildDockerFileInstructions(ctx, {
294
- image,
295
- tag,
296
- ignorePHPExtensions: ['xdebug']
297
- })
298
288
 
299
- try {
300
- await execAsyncSpawn(
301
- `docker build -t ${containers.php.image} -<<EOF
289
+ return task.newListr([
290
+ {
291
+ title: 'Building PHP image',
292
+ task: async () => {
293
+ const [image, tag = 'latest'] =
294
+ ctx.config.overridenConfiguration.configuration.php.baseImage.split(
295
+ ':'
296
+ )
297
+ const dockerFileInstructions =
298
+ await buildDockerFileInstructions(ctx, {
299
+ image,
300
+ tag,
301
+ ignorePHPExtensions: ['xdebug']
302
+ })
303
+
304
+ try {
305
+ await execAsyncSpawn(
306
+ `docker build -t ${containers.php.image} -<<EOF
302
307
  ${dockerFileInstructions}
303
308
  EOF`,
304
- {
305
- callback: (r) => {
306
- task.output = r
309
+ {
310
+ callback: (r) => {
311
+ task.output = r
312
+ }
313
+ }
314
+ )
315
+ } catch (e) {
316
+ throw new KnownError(
317
+ `Unexpected error during PHP image building!\n\n${e}`
318
+ )
307
319
  }
308
320
  }
309
- )
310
- } catch (e) {
311
- throw new KnownError(
312
- `Unexpected error during project image building!\n\n${e}`
313
- )
314
- }
315
-
316
- const [phpImage, phpTag] = containers.php.image.split(':')
317
- const debugImageInstructions = await buildDebugDockerFileInstructions(
318
- ctx,
321
+ },
319
322
  {
320
- image: phpImage,
321
- tag: phpTag
322
- }
323
- )
324
-
325
- try {
326
- await execAsyncSpawn(
327
- `docker build -t ${containers.phpWithXdebug.image} -<<EOF
323
+ title: 'Building PHP with XDebug image',
324
+ task: async () => {
325
+ const [phpImage, phpTag] = containers.php.image.split(':')
326
+ const debugImageInstructions =
327
+ await buildDebugDockerFileInstructions(ctx, {
328
+ image: phpImage,
329
+ tag: phpTag
330
+ })
331
+
332
+ try {
333
+ await execAsyncSpawn(
334
+ `docker build -t ${containers.phpWithXdebug.image} -<<EOF
328
335
  ${debugImageInstructions}
329
336
  EOF`,
330
- {
331
- callback: (r) => {
332
- task.output = r
337
+ {
338
+ callback: (r) => {
339
+ task.output = r
340
+ }
341
+ }
342
+ )
343
+ } catch (e) {
344
+ throw new KnownError(
345
+ `Unexpected error during PHP with XDebug image building!\n\n${e}`
346
+ )
333
347
  }
334
348
  }
335
- )
336
- } catch (e) {
337
- throw new KnownError(
338
- `Unexpected error during project image building!\n\n${e}`
339
- )
340
- }
349
+ }
350
+ ])
341
351
  },
342
352
  options: {
343
353
  bottomBar: 10
@@ -69,3 +69,69 @@ export function df(
69
69
  options?: SystemDFOptions<true>,
70
70
  execOptions?: ExecAsyncSpawnOptions<false>
71
71
  ): Promise<SystemDFResult>
72
+
73
+ export interface SystemVersionOptions<T extends boolean = false> {
74
+ format?: string
75
+ formatToJSON?: T
76
+ verbose?: boolean
77
+ }
78
+
79
+ interface DockerServiceComponent {
80
+ Name: string;
81
+ Version: string;
82
+ Details: {
83
+ ApiVersion?: string;
84
+ Arch?: string;
85
+ BuildTime?: string;
86
+ Experimental?: string;
87
+ GitCommit?: string;
88
+ GoVersion?: string;
89
+ KernelVersion?: string;
90
+ MinAPIVersion?: string;
91
+ Os?: string;
92
+ };
93
+ }
94
+ interface DockerServerInfo {
95
+ Platform: {
96
+ Name: string;
97
+ };
98
+ Components: DockerServiceComponent[];
99
+ Version: string;
100
+ ApiVersion: string;
101
+ MinAPIVersion: string;
102
+ GitCommit: string;
103
+ GoVersion: string;
104
+ Os: string;
105
+ Arch: string;
106
+ KernelVersion: string;
107
+ BuildTime: string;
108
+ }
109
+
110
+ interface DockerClientInfo {
111
+ Platform: {
112
+ Name: string;
113
+ };
114
+ Version: string;
115
+ ApiVersion: string;
116
+ DefaultAPIVersion: string;
117
+ GitCommit: string;
118
+ GoVersion: string;
119
+ Os: string;
120
+ Arch: string;
121
+ BuildTime: string;
122
+ Context: string;
123
+ }
124
+
125
+ export interface SystemVersionResult {
126
+ Client: DockerClientInfo
127
+ Server: DockerServerInfo
128
+ }
129
+
130
+ export function version(
131
+ options?: SystemVersionOptions,
132
+ execOptions?: ExecAsyncSpawnOptions<false>
133
+ ): Promise<string>
134
+ export function version(
135
+ options?: SystemVersionOptions<true>,
136
+ execOptions?: ExecAsyncSpawnOptions<false>
137
+ ): Promise<SystemVersionResult>
@@ -27,6 +27,33 @@ const df = async (options, execOptions = {}) => {
27
27
  return execAsyncSpawn(`docker system df ${args}`, execOptions)
28
28
  }
29
29
 
30
+ /**
31
+ * @param {import('./system-api').SystemVersionOptions} options
32
+ * @param {import('../../../util/exec-async-command').ExecAsyncSpawnOptions} execOptions
33
+ */
34
+ const version = async (options, execOptions = {}) => {
35
+ const { format, formatToJSON } = options
36
+
37
+ const formatArg =
38
+ !formatToJSON && format
39
+ ? `--format=${format}`
40
+ : formatToJSON && "--format='{{json .}}'"
41
+
42
+ const args = [formatArg].filter(Boolean).join(' ')
43
+
44
+ if (formatToJSON) {
45
+ const result = await execAsyncSpawn(
46
+ `docker version ${args}`,
47
+ execOptions
48
+ )
49
+
50
+ return JSON.parse(result)
51
+ }
52
+
53
+ return execAsyncSpawn(`docker version ${args}`, execOptions)
54
+ }
55
+
30
56
  module.exports = {
31
- df
57
+ df,
58
+ version
32
59
  }
@@ -102,7 +102,7 @@ const executeTask = async (argv) => {
102
102
  return result
103
103
  }
104
104
 
105
- if (container.name.endsWith('php')) {
105
+ if (container.name.includes('php')) {
106
106
  if (process.stdout.isTTY) {
107
107
  logger.logN(
108
108
  `Starting container ${logger.style.misc(
@@ -10,7 +10,7 @@ const createNginxConfig = () => ({
10
10
  task: async (ctx) => {
11
11
  const {
12
12
  ports,
13
- config: { overridenConfiguration, baseConfig },
13
+ config: { overridenConfiguration, baseConfig, docker },
14
14
  isDockerDesktop
15
15
  } = ctx
16
16
 
@@ -19,10 +19,26 @@ const createNginxConfig = () => ({
19
19
  storeDomains
20
20
  } = overridenConfiguration
21
21
 
22
- const hostMachine = !isDockerDesktop
23
- ? '127.0.0.1'
24
- : 'host.docker.internal'
25
- const hostPort = !isDockerDesktop ? ports.app : 80
22
+ const networkSettings = {
23
+ phpNetwork: '127.0.0.1',
24
+ phpWithXdebugNetwork: '127.0.0.1',
25
+ fpmPort: ports.fpm,
26
+ fpmXdebugPort: ports.fpmXdebug,
27
+ hostPort: ports.app
28
+ }
29
+
30
+ if (isDockerDesktop) {
31
+ const containers = docker.getContainers(ports)
32
+
33
+ networkSettings.phpNetwork = containers.php.name
34
+ networkSettings.phpWithXdebugNetwork = containers.phpWithXdebug.name
35
+
36
+ networkSettings.fpmPort = 9000
37
+ networkSettings.fpmXdebugPort = 9001
38
+
39
+ networkSettings.hostPort = 80
40
+ }
41
+
26
42
  const useStoreDomainMapping =
27
43
  storeDomains && Object.keys(storeDomains).length > 1
28
44
 
@@ -37,10 +53,8 @@ const createNginxConfig = () => ({
37
53
  template: nginx.configTemplate,
38
54
  overwrite: true,
39
55
  templateArgs: {
40
- ports,
56
+ ...networkSettings,
41
57
  mageRoot: baseConfig.containerMagentoDir,
42
- hostMachine,
43
- hostPort,
44
58
  config: overridenConfiguration,
45
59
  storeDomains,
46
60
  useStoreDomainMapping
@@ -15,8 +15,7 @@ const createSSLTerminatorConfig = () => ({
15
15
  task: async (ctx) => {
16
16
  const {
17
17
  ports,
18
- config: { overridenConfiguration, baseConfig },
19
- debug,
18
+ config: { overridenConfiguration, baseConfig, docker },
20
19
  isDockerDesktop
21
20
  } = ctx
22
21
 
@@ -72,9 +71,24 @@ const createSSLTerminatorConfig = () => ({
72
71
  )
73
72
  }
74
73
 
75
- const hostMachine = !isDockerDesktop
76
- ? '127.0.0.1'
77
- : 'host.docker.internal'
74
+ const networkSettings = {
75
+ backendNetwork: '127.0.0.1',
76
+ backendPort: overridenConfiguration.configuration.varnish.enabled
77
+ ? ports.varnish
78
+ : ports.app
79
+ }
80
+
81
+ if (isDockerDesktop) {
82
+ const containers = docker.getContainers(ports)
83
+
84
+ if (overridenConfiguration.configuration.varnish.enabled) {
85
+ networkSettings.backendNetwork = containers.varnish.name
86
+ networkSettings.backendPort = 80
87
+ } else {
88
+ networkSettings.backendNetwork = containers.nginx.name
89
+ networkSettings.backendPort = 80
90
+ }
91
+ }
78
92
  const hostPort = !isDockerDesktop ? ports.sslTerminator : 80
79
93
 
80
94
  const nginxVersionOutput = await run({
@@ -112,10 +126,9 @@ const createSSLTerminatorConfig = () => ({
112
126
  overwrite: true,
113
127
  templateArgs: {
114
128
  ports,
115
- hostMachine,
129
+ ...networkSettings,
116
130
  hostPort,
117
131
  config: overridenConfiguration,
118
- debug,
119
132
  isSSLDirectiveDeprecated
120
133
  }
121
134
  })
@@ -9,6 +9,11 @@ const getJsonFileData = require('../../util/get-jsonfile-data')
9
9
  const KnownError = require('../../errors/known-error')
10
10
  const UnknownError = require('../../errors/unknown-error')
11
11
  const { runPHPContainerCommand } = require('../php/php-container')
12
+ const {
13
+ setupMagentoFilePermissions,
14
+ setupComposerCachePermissions
15
+ } = require('./setup-magento/setup-file-permissions')
16
+ const makeBinariesExecutable = require('./setup-magento/make-magento-binaries-executable')
12
17
 
13
18
  const magentoProductEnterpriseEdition = 'magento/product-enterprise-edition'
14
19
  const magentoProductCommunityEdition = 'magento/product-community-edition'
@@ -59,7 +64,8 @@ const adjustComposerJson = async (
59
64
  ? undefined
60
65
  : (t) => {
61
66
  task.output = t
62
- }
67
+ },
68
+ useAutomaticUser: true
63
69
  }
64
70
  )
65
71
  }
@@ -213,30 +219,40 @@ const installMagentoProject = () => ({
213
219
  })
214
220
  }
215
221
 
216
- try {
217
- await runComposerCommand(ctx, 'install', {
218
- callback: !ctx.verbose
219
- ? undefined
220
- : (t) => {
221
- task.output = t
222
- }
223
- })
224
- } catch (e) {
225
- if (
226
- e instanceof UnknownError &&
227
- e.message.includes('man-in-the-middle attack')
228
- ) {
229
- throw new KnownError(`Probably you haven't setup pubkeys in composer.
230
- Please run ${logger.style.command(
231
- 'composer diagnose'
232
- )} in cli to get mode.\n\n${e}`)
233
- }
222
+ return task.newListr([
223
+ setupMagentoFilePermissions(),
224
+ setupComposerCachePermissions(),
225
+ {
226
+ title: 'Installing Magento dependencies',
227
+ task: async () => {
228
+ try {
229
+ await runComposerCommand(ctx, 'install', {
230
+ callback: !ctx.verbose
231
+ ? undefined
232
+ : (t) => {
233
+ task.output = t
234
+ }
235
+ })
236
+ } catch (e) {
237
+ if (
238
+ e instanceof UnknownError &&
239
+ e.message.includes('man-in-the-middle attack')
240
+ ) {
241
+ throw new KnownError(`Probably you haven't setup pubkeys in composer.
242
+ Please run ${logger.style.command(
243
+ 'composer diagnose'
244
+ )} in cli to get mode.\n\n${e}`)
245
+ }
234
246
 
235
- throw new UnknownError(
236
- `Unexpected error during composer install.\n\n${e}`
237
- )
238
- }
239
- ctx.magentoFirstInstall = true
247
+ throw new UnknownError(
248
+ `Unexpected error during composer install.\n\n${e}`
249
+ )
250
+ }
251
+ ctx.magentoFirstInstall = true
252
+ }
253
+ },
254
+ makeBinariesExecutable()
255
+ ])
240
256
  },
241
257
  options: {
242
258
  bottomBar: 10
@@ -0,0 +1,32 @@
1
+ <?php
2
+ $directories = [
3
+ '/var',
4
+ '/generated',
5
+ '/vendor',
6
+ '/pub/static',
7
+ '/pub/media',
8
+ '/app/etc',
9
+ ];
10
+
11
+ $results = [];
12
+
13
+ foreach ($directories as $dir) {
14
+ $directory = getcwd() . $dir;
15
+ $result = [];
16
+ $result['exists'] = file_exists($directory);
17
+ $result['writable'] = is_writable($directory);
18
+ $result['readable'] = is_readable($directory);
19
+
20
+ if ($result['exists']) {
21
+ $result['owner'] = posix_getpwuid(fileowner($directory));
22
+ $result['group'] = posix_getgrgid(filegroup($directory));
23
+ $result['current_user'] = posix_getpwuid(posix_geteuid());
24
+
25
+ $result['permissions'] =substr(sprintf('%o', fileperms($directory)), -4);
26
+ $result['is_current_user_directory_owner'] = $result['current_user'] === $result['owner'];
27
+ }
28
+
29
+ $results[] = $result;
30
+ }
31
+
32
+ echo json_encode($results, JSON_PRETTY_PRINT);
@@ -13,6 +13,7 @@ const urnHighlighter = require('./urn-highlighter')
13
13
  const adjustFullPageCache = require('./adjust-full-page-cache')
14
14
  const updateEnvPHP = require('../../php/update-env-php')
15
15
  const setMailConfig = require('./set-mail-config')
16
+ const { setupMagentoFilePermissions } = require('./setup-file-permissions')
16
17
 
17
18
  /**
18
19
  * @param {Object} [options]
@@ -34,6 +35,7 @@ const setupMagento = (options = {}) => ({
34
35
  return task.newListr(
35
36
  [
36
37
  waitingForRedis(),
38
+ setupMagentoFilePermissions(),
37
39
  updateEnvPHP(),
38
40
  migrateDatabase(),
39
41
  flushRedisConfig(),
@@ -0,0 +1,44 @@
1
+ // Yes, I know binary files are files in binary format, but this is a common term for executable files
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const pathExists = require('../../../util/path-exists')
6
+
7
+ /**
8
+ * @returns {import('listr2').ListrTask<import('../../../../typings/context').ListrContext>}
9
+ */
10
+ const makeBinariesExecutable = () => ({
11
+ task: async (ctx, task) => {
12
+ const directoriesWithBinFiles = [
13
+ path.join(ctx.config.baseConfig.magentoDir, 'bin'),
14
+ path.join(ctx.config.baseConfig.magentoDir, 'vendor', 'bin')
15
+ ]
16
+
17
+ for (const directory of directoriesWithBinFiles) {
18
+ if (!(await pathExists(directory))) {
19
+ continue
20
+ }
21
+ const files = await fs.promises.readdir(directory, {
22
+ withFileTypes: true
23
+ })
24
+
25
+ for (const file of files) {
26
+ if (!file.isFile()) {
27
+ continue
28
+ }
29
+
30
+ const filePath = path.join(directory, file.name)
31
+ const stats = await fs.promises.stat(filePath)
32
+
33
+ if (!filePath.startsWith(file.name) && stats.mode & 0o111) {
34
+ continue
35
+ }
36
+
37
+ task.output = `Making ${filePath} executable`
38
+ await fs.promises.chmod(filePath, 0o755)
39
+ }
40
+ }
41
+ }
42
+ })
43
+
44
+ module.exports = makeBinariesExecutable