@scandipwa/magento-scripts 2.4.11 → 2.4.12-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -6,6 +6,7 @@ const logger = require('@scandipwa/scandipwa-dev-utils/logger')
6
6
  const semver = require('semver')
7
7
  const isInstalledGlobally = require('is-installed-globally')
8
8
  const isRunningRoot = require('./lib/util/is-running-root')
9
+ const ensureAgentsMd = require('./lib/util/ensure-agents-md')
9
10
 
10
11
  if (isRunningRoot()) {
11
12
  logger.error('Root privileges detected!')
@@ -27,6 +28,8 @@ If you are experiencing problems with ${logger.style.misc(
27
28
  process.exit(1)
28
29
  }
29
30
 
31
+ ensureAgentsMd()
32
+
30
33
  const commands = [
31
34
  require('./lib/commands/link'),
32
35
  require('./lib/commands/logs'),
@@ -123,6 +126,14 @@ const newVersionIsAPatch = (latestVersion, currentVersion) => {
123
126
  yargs.scriptName('magento-scripts')
124
127
  yargs.version(false)
125
128
 
129
+ yargs.option('silent', {
130
+ alias: 'q',
131
+ describe:
132
+ 'Suppress all task progress output (Listr silent renderer). Enabled automatically when stdout is not a TTY.',
133
+ type: 'boolean',
134
+ default: false
135
+ })
136
+
126
137
  // Initialize program commands
127
138
  commands.forEach((command) => command(yargs))
128
139
 
@@ -18,12 +18,16 @@ module.exports = (yargs) => {
18
18
  }),
19
19
  async (args) => {
20
20
  logger.warn('you should not use this command.')
21
+ const silent = /** @type {boolean} */ (args.silent)
21
22
  const tasks = new Listr(cleanup(), {
22
23
  concurrent: false,
23
24
  exitOnError: true,
24
25
  ctx: {
25
- force: args.force
26
+ force: args.force,
27
+ silent
26
28
  },
29
+ renderer:
30
+ silent || !process.stdout.isTTY ? 'silent' : 'default',
27
31
  rendererOptions: { collapse: false }
28
32
  })
29
33
 
@@ -20,7 +20,8 @@ module.exports = (yargs) => {
20
20
  'Enter CLI (magento, php, composer).',
21
21
  // eslint-disable-next-line @typescript-eslint/no-empty-function
22
22
  () => {},
23
- async () => {
23
+ async (args) => {
24
+ const silent = /** @type {boolean} */ (args.silent)
24
25
  const tasks = new Listr(
25
26
  [
26
27
  getMagentoVersionConfig(),
@@ -33,8 +34,11 @@ module.exports = (yargs) => {
33
34
  concurrent: false,
34
35
  exitOnError: true,
35
36
  ctx: {
36
- throwMagentoVersionMissing: true
37
+ throwMagentoVersionMissing: true,
38
+ silent
37
39
  },
40
+ renderer:
41
+ silent || !process.stdout.isTTY ? 'silent' : 'default',
38
42
  rendererOptions: { collapse: false, clearOutput: true }
39
43
  }
40
44
  )
@@ -27,12 +27,25 @@ module.exports = (yargs) => {
27
27
  type: 'boolean',
28
28
  default: false
29
29
  })
30
+ yargs.option('non-interactive', {
31
+ alias: 'y',
32
+ describe:
33
+ 'Skip all interactive prompts using sensible defaults',
34
+ type: 'boolean',
35
+ default: false
36
+ })
30
37
  },
31
38
  async (args) => {
32
39
  const tasks = new Listr(importDump(), {
33
40
  exitOnError: true,
34
41
  ctx: args,
35
42
  concurrent: false,
43
+ renderer:
44
+ /** @type {boolean} */ (args.silent) ||
45
+ /** @type {boolean} */ (args.nonInteractive) ||
46
+ !process.stdout.isTTY
47
+ ? 'silent'
48
+ : 'default',
36
49
  rendererOptions: {
37
50
  showErrorMessage: false,
38
51
  showTimer: true
@@ -17,12 +17,18 @@ module.exports = (yargs) => {
17
17
  default: false
18
18
  }),
19
19
  async (args) => {
20
- const tasks = new Listr(linkTask(args.themepath), {
21
- concurrent: false,
22
- exitOnError: true,
23
- ctx: { throwMagentoVersionMissing: true },
24
- rendererOptions: { collapse: false }
25
- })
20
+ const silent = /** @type {boolean} */ (args.silent)
21
+ const tasks = new Listr(
22
+ linkTask(/** @type {string} */ (args.themepath)),
23
+ {
24
+ concurrent: false,
25
+ exitOnError: true,
26
+ ctx: { throwMagentoVersionMissing: true, silent },
27
+ renderer:
28
+ silent || !process.stdout.isTTY ? 'silent' : 'default',
29
+ rendererOptions: { collapse: false }
30
+ }
31
+ )
26
32
 
27
33
  try {
28
34
  await tasks.run()
@@ -70,6 +70,7 @@ npm run logs re (will match redis)`)
70
70
  })
71
71
  },
72
72
  async (argv) => {
73
+ const silent = /** @type {boolean} */ (argv.silent)
73
74
  const tasks = new Listr(
74
75
  [
75
76
  checkRequirements(),
@@ -82,7 +83,12 @@ npm run logs re (will match redis)`)
82
83
  {
83
84
  concurrent: false,
84
85
  exitOnError: true,
85
- ctx: { throwMagentoVersionMissing: true },
86
+ ctx: {
87
+ throwMagentoVersionMissing: true,
88
+ silent
89
+ },
90
+ renderer:
91
+ silent || !process.stdout.isTTY ? 'silent' : 'default',
86
92
  rendererOptions: { collapse: false, clearOutput: true }
87
93
  }
88
94
  )
@@ -64,9 +64,9 @@ module.exports = (yargs) => {
64
64
  type: 'number',
65
65
  nargs: 1
66
66
  })
67
- .option('no-open', {
67
+ .option('open', {
68
68
  alias: 'n',
69
- describe: 'Do not open browser after command finished',
69
+ describe: 'Open browser after command finished',
70
70
  type: 'boolean',
71
71
  default: false
72
72
  })
@@ -109,6 +109,11 @@ module.exports = (yargs) => {
109
109
  exitOnError: true,
110
110
  ctx: args,
111
111
  concurrent: false,
112
+ renderer:
113
+ /** @type {boolean} */ (args.silent) ||
114
+ !process.stdout.isTTY
115
+ ? 'silent'
116
+ : 'default',
112
117
  rendererOptions: {
113
118
  showErrorMessage: false,
114
119
  showTimer: true
@@ -23,6 +23,7 @@ module.exports = (yargs) => {
23
23
  // eslint-disable-next-line @typescript-eslint/no-empty-function
24
24
  () => {},
25
25
  async (args) => {
26
+ const silent = /** @type {boolean} */ (args.silent)
26
27
  const tasks = new Listr(
27
28
  [
28
29
  checkRequirements(),
@@ -58,7 +59,9 @@ module.exports = (yargs) => {
58
59
  {
59
60
  concurrent: false,
60
61
  exitOnError: false,
61
- ctx: { throwMagentoVersionMissing: true, ...args },
62
+ ctx: { throwMagentoVersionMissing: true, ...args, silent },
63
+ renderer:
64
+ silent || !process.stdout.isTTY ? 'silent' : 'default',
62
65
  rendererOptions: { collapse: false, clearOutput: false }
63
66
  }
64
67
  )
@@ -10,16 +10,20 @@ module.exports = (yargs) => {
10
10
  'Stop the application.',
11
11
  // eslint-disable-next-line @typescript-eslint/no-empty-function
12
12
  () => {},
13
- async () => {
13
+ async (args) => {
14
+ const silent = /** @type {boolean} */ (args.silent)
14
15
  const tasks = new Listr(stop(), {
15
16
  concurrent: false,
16
17
  exitOnError: true,
17
- rendererOptions: {
18
- collapse: false
19
- },
20
18
  ctx: {
21
19
  throwMagentoVersionMissing: true,
22
- projectPath: process.cwd()
20
+ projectPath: process.cwd(),
21
+ silent
22
+ },
23
+ renderer:
24
+ silent || !process.stdout.isTTY ? 'silent' : 'default',
25
+ rendererOptions: {
26
+ collapse: false
23
27
  }
24
28
  })
25
29
 
@@ -49,13 +49,15 @@ const checkConfigurationFile = () => ({
49
49
  }
50
50
 
51
51
  if (!magentoConfiguration) {
52
- const magentoEdition = await task.prompt({
53
- type: 'Select',
54
- message: `Please select Magento edition you want to install.
52
+ const magentoEdition = ctx.nonInteractive
53
+ ? 'Community'
54
+ : await task.prompt({
55
+ type: 'Select',
56
+ message: `Please select Magento edition you want to install.
55
57
 
56
58
  Note that Enterprise edition requires Magento Enterprise License keys.`,
57
- choices: ['Community', 'Enterprise']
58
- })
59
+ choices: ['Community', 'Enterprise']
60
+ })
59
61
 
60
62
  magentoConfiguration = deepmerge(defaultMagentoConfig, {
61
63
  edition: magentoEdition.toLowerCase()
@@ -232,7 +232,8 @@ module.exports = async (ctx, overridenConfiguration, baseConfig) => {
232
232
  name: `${prefix}_php_with_xdebug`,
233
233
  connectCommand: ['/bin/sh'],
234
234
  execCommandEnv: {
235
- XDEBUG_TRIGGER: 'PHPSTORM'
235
+ XDEBUG_TRIGGER: 'PHPSTORM',
236
+ XDEBUG_SESSION: 'PHPSTORM'
236
237
  },
237
238
  dependsOn: ['mariadb', 'redis', 'elasticsearch'],
238
239
  user:
@@ -48,6 +48,8 @@ const getMagentoVersion = () => ({
48
48
  const { magentoVersion: defaultMagentoVersion } =
49
49
  defaultConfiguration
50
50
  magentoVersion = defaultMagentoVersion
51
+ } else if (ctx.nonInteractive) {
52
+ magentoVersion = defaultConfiguration.magento
51
53
  } else {
52
54
  let promptSkipper = false
53
55
  const timer = async () => {
@@ -1,4 +1,5 @@
1
1
  const mysql2 = require('mysql2/promise')
2
+ const sleep = require('../../util/sleep')
2
3
  const defaultMagentoUser = require('./default-magento-user')
3
4
 
4
5
  /**
@@ -8,12 +9,31 @@ const createMagentoUser = () => ({
8
9
  title: 'Creating Magento user',
9
10
  task: async (ctx, task) => {
10
11
  const { mariadb } = ctx.config.docker.getContainers()
11
- const connection = await mysql2.createConnection({
12
- host: '127.0.0.1',
13
- port: ctx.ports.mariadb,
14
- user: 'root',
15
- password: mariadb.env.MARIADB_ROOT_PASSWORD
16
- })
12
+
13
+ /** @type {import('mysql2/promise').Connection | undefined} */
14
+ let connection
15
+ const maxTries = 20
16
+
17
+ for (let tries = 1; tries <= maxTries; tries++) {
18
+ try {
19
+ connection = await mysql2.createConnection({
20
+ host: '127.0.0.1',
21
+ port: ctx.ports.mariadb,
22
+ user: 'root',
23
+ password: mariadb.env.MARIADB_ROOT_PASSWORD
24
+ })
25
+ break
26
+ } catch (e) {
27
+ if (tries === maxTries) {
28
+ throw e
29
+ }
30
+ await sleep(1000)
31
+ }
32
+ }
33
+
34
+ if (!connection) {
35
+ throw new Error('Failed to connect to MariaDB')
36
+ }
17
37
 
18
38
  const result = await connection.query(
19
39
  'select Host, User from mysql.user;'
@@ -36,11 +36,13 @@ const fixDB = () => ({
36
36
  title: 'Deleting customers data',
37
37
  skip: ({ withCustomersData }) => withCustomersData,
38
38
  task: async (ctx, subTask) => {
39
- const deleteCustomerData = await subTask.prompt({
40
- type: 'Confirm',
41
- message: `Do you want to delete customers data (orders, customers and admin users) from this dump?
39
+ const deleteCustomerData = ctx.nonInteractive
40
+ ? false
41
+ : await subTask.prompt({
42
+ type: 'Confirm',
43
+ message: `Do you want to delete customers data (orders, customers and admin users) from this dump?
42
44
  This will reduce database size and remove possible interference for your setup.`
43
- })
45
+ })
44
46
 
45
47
  if (!deleteCustomerData) {
46
48
  subTask.skip()
@@ -60,25 +60,27 @@ const runSetGlobalLogBinTrustFunctionCreatorsCommand = () => ({
60
60
  const deleteDatabaseBeforeImportingDumpPrompt = () => ({
61
61
  title: 'Deleting magento database before importing dump',
62
62
  task: async (ctx, task) => {
63
- const deleteDatabaseMagentoChoice = await task.prompt({
64
- type: 'Select',
65
- message: `Before importing database dump, would you like to delete existing database?
63
+ const deleteDatabaseMagentoChoice = ctx.nonInteractive
64
+ ? 'delete'
65
+ : await task.prompt({
66
+ type: 'Select',
67
+ message: `Before importing database dump, would you like to delete existing database?
66
68
 
67
69
  It is possible that dump might interfere with existing data in database.
68
70
 
69
71
  Note that you will lose your existing database!`,
70
- choices: [
71
- {
72
- name: 'delete',
73
- message: 'YES I WANT TO DELETE magento DATABASE!'
74
- },
75
- {
76
- name: 'skip',
77
- message:
78
- "NO I DON'T WANT TO DELETE magento DATABASE! (Skip this step)"
79
- }
80
- ]
81
- })
72
+ choices: [
73
+ {
74
+ name: 'delete',
75
+ message: 'YES I WANT TO DELETE magento DATABASE!'
76
+ },
77
+ {
78
+ name: 'skip',
79
+ message:
80
+ "NO I DON'T WANT TO DELETE magento DATABASE! (Skip this step)"
81
+ }
82
+ ]
83
+ })
82
84
 
83
85
  if (deleteDatabaseMagentoChoice === 'delete') {
84
86
  await ctx.databaseConnection.query(
@@ -103,22 +105,24 @@ const executeImportDumpSQL = () => ({
103
105
  const { mariadb } = docker.getContainers(ports)
104
106
  const { binFileName } = overridenConfiguration.configuration.mariadb
105
107
 
106
- const userCredentialsForMariaDBCLI = await task.prompt({
107
- type: 'Select',
108
- message: `Which user do you want to use to import db in ${mariadb._} client?`,
109
- choices: [
110
- {
111
- name: `--user=root --password=${mariadb.env.MARIADB_ROOT_PASSWORD}`,
112
- message: `root (${logger.style.command(
113
- 'Probably safest option'
114
- )})`
115
- },
116
- {
117
- name: `--user=${defaultMagentoUser.user} --password=${defaultMagentoUser.password}`,
118
- message: `${defaultMagentoUser.user}`
119
- }
120
- ]
121
- })
108
+ const userCredentialsForMariaDBCLI = ctx.nonInteractive
109
+ ? `--user=root --password=${mariadb.env.MARIADB_ROOT_PASSWORD}`
110
+ : await task.prompt({
111
+ type: 'Select',
112
+ message: `Which user do you want to use to import db in ${mariadb._} client?`,
113
+ choices: [
114
+ {
115
+ name: `--user=root --password=${mariadb.env.MARIADB_ROOT_PASSWORD}`,
116
+ message: `root (${logger.style.command(
117
+ 'Probably safest option'
118
+ )})`
119
+ },
120
+ {
121
+ name: `--user=${defaultMagentoUser.user} --password=${defaultMagentoUser.password}`,
122
+ message: `${defaultMagentoUser.user}`
123
+ }
124
+ ]
125
+ })
122
126
 
123
127
  const importCommand = `docker exec ${mariadb.name} bash -c "${binFileName} ${userCredentialsForMariaDBCLI} magento < ./dump.sql"`
124
128
 
@@ -137,11 +141,13 @@ const executeImportDumpSQL = () => ({
137
141
  })
138
142
  } catch (e) {
139
143
  if (e.message.includes("Unknown collation: 'utf8mb4_0900_ai_ci'")) {
140
- const confirmFixingCollation = await task.prompt({
141
- type: 'Select',
142
- message: `We got the following error while trying to import ${logger.style.file(
143
- 'dump.sql'
144
- )}!
144
+ const confirmFixingCollation = ctx.nonInteractive
145
+ ? 'yes'
146
+ : await task.prompt({
147
+ type: 'Select',
148
+ message: `We got the following error while trying to import ${logger.style.file(
149
+ 'dump.sql'
150
+ )}!
145
151
 
146
152
  ${e.message}
147
153
 
@@ -150,18 +156,19 @@ ${logger.style.command(
150
156
  "sed -i 's/utf8mb4_0900_ai_ci/utf8mb4_general_ci/g' dump.sql"
151
157
  )}
152
158
  `,
153
- choices: [
154
- {
155
- name: 'yes',
156
- message:
157
- 'Yes, run the following commands, I reaaaalllyy want dump to work! (this will not edit original dump.sql)'
158
- },
159
- {
160
- name: 'no',
161
- message: 'Okay, I got it. Will try to fix myself'
162
- }
163
- ]
164
- })
159
+ choices: [
160
+ {
161
+ name: 'yes',
162
+ message:
163
+ 'Yes, run the following commands, I reaaaalllyy want dump to work! (this will not edit original dump.sql)'
164
+ },
165
+ {
166
+ name: 'no',
167
+ message:
168
+ 'Okay, I got it. Will try to fix myself'
169
+ }
170
+ ]
171
+ })
165
172
 
166
173
  if (confirmFixingCollation === 'yes') {
167
174
  task.output = 'Running fix command...'
@@ -19,11 +19,13 @@ const sshDb = () => ({
19
19
  ctx.ssh = ssh
20
20
 
21
21
  if (!password) {
22
- const privateKey = await task.prompt({
23
- type: 'Input',
24
- message: `Please enter your private key location to connect to ${hostname}\n`,
25
- initial: `${os.homedir()}/.ssh/id_rsa`
26
- })
22
+ const privateKey = ctx.nonInteractive
23
+ ? `${os.homedir()}/.ssh/id_rsa`
24
+ : await task.prompt({
25
+ type: 'Input',
26
+ message: `Please enter your private key location to connect to ${hostname}\n`,
27
+ initial: `${os.homedir()}/.ssh/id_rsa`
28
+ })
27
29
 
28
30
  if (!(await pathExists(privateKey))) {
29
31
  throw new KnownError(`Private key not found: ${privateKey}`)
@@ -31,11 +33,13 @@ const sshDb = () => ({
31
33
 
32
34
  ctx.privateKey = privateKey
33
35
 
34
- const passphrase = await task.prompt({
35
- type: 'Input',
36
- message:
37
- 'Please enter your private key passphrase (if you have it)'
38
- })
36
+ const passphrase = ctx.nonInteractive
37
+ ? undefined
38
+ : await task.prompt({
39
+ type: 'Input',
40
+ message:
41
+ 'Please enter your private key passphrase (if you have it)'
42
+ })
39
43
 
40
44
  ctx.passphrase = passphrase || undefined
41
45
 
@@ -67,14 +71,16 @@ const sshDb = () => ({
67
71
  const remoteFiles = remoteFilesOutput.split('\n')
68
72
 
69
73
  if (dumpFileNames.every((dumpFile) => remoteFiles.includes(dumpFile))) {
70
- ctx.makeRemoteDumps = await task.prompt({
71
- type: 'Toggle',
72
- enabled: 'Yes!',
73
- disabled: 'No, just download and import them.',
74
- message: `We found dump files on remote server.
74
+ ctx.makeRemoteDumps = ctx.nonInteractive
75
+ ? true
76
+ : await task.prompt({
77
+ type: 'Toggle',
78
+ enabled: 'Yes!',
79
+ disabled: 'No, just download and import them.',
80
+ message: `We found dump files on remote server.
75
81
  Do you want to replace them with new dump files or use existing ones?
76
82
  `
77
- })
83
+ })
78
84
  } else {
79
85
  ctx.makeRemoteDumps = true
80
86
  }
@@ -20,15 +20,17 @@ const regularSSHServer = () => ({
20
20
  /**
21
21
  * @type {string}
22
22
  */
23
- const dumpCommand = await task.prompt({
24
- type: 'Input',
25
- message: `Edit (if needed) command to connect to remote mysql server and create dump files.
23
+ const dumpCommand = ctx.nonInteractive
24
+ ? databaseDumpCommandWithOptions.join(' ')
25
+ : await task.prompt({
26
+ type: 'Input',
27
+ message: `Edit (if needed) command to connect to remote mysql server and create dump files.
26
28
  Do not enter "--result-file" option, we need to control that part.
27
29
 
28
30
  (documentation reference available here: https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html)
29
31
  `,
30
- initial: databaseDumpCommandWithOptions.join(' ')
31
- })
32
+ initial: databaseDumpCommandWithOptions.join(' ')
33
+ })
32
34
 
33
35
  if (dumpCommand.includes('--result-file')) {
34
36
  throw new KnownError(
@@ -38,12 +38,9 @@ const executeTask = async (argv) => {
38
38
  {
39
39
  concurrent: false,
40
40
  exitOnError: true,
41
- ctx: /** @type {any} */ ({ throwMagentoVersionMissing: true }),
42
- renderer: nonInteractive
43
- ? 'silent'
44
- : process.stdout.isTTY
45
- ? 'default'
46
- : 'silent',
41
+ ctx: { throwMagentoVersionMissing: true, nonInteractive },
42
+ renderer:
43
+ nonInteractive || !process.stdout.isTTY ? 'silent' : 'default',
47
44
  rendererOptions: { collapse: false, clearOutput: true }
48
45
  }
49
46
  )
@@ -51,22 +51,24 @@ const importDump = () => ({
51
51
  },
52
52
  task: async (subCtx, subTask) => {
53
53
  const doYouWantToRunSetupOnEmptyDB =
54
- await subTask.prompt({
55
- type: 'Select',
56
- message: `We detected that Magento is not installed in database. Do you want to install Magento in database BEFORE importing database dump?`,
57
- choices: [
58
- {
59
- name: 'try-install',
60
- message:
61
- 'Try installing Magento before importing database'
62
- },
63
- {
64
- name: 'skip',
65
- message:
66
- 'Skip installing Magento and import database dump right away!'
67
- }
68
- ]
69
- })
54
+ subCtx.nonInteractive
55
+ ? 'skip'
56
+ : await subTask.prompt({
57
+ type: 'Select',
58
+ message: `We detected that Magento is not installed in database. Do you want to install Magento in database BEFORE importing database dump?`,
59
+ choices: [
60
+ {
61
+ name: 'try-install',
62
+ message:
63
+ 'Try installing Magento before importing database'
64
+ },
65
+ {
66
+ name: 'skip',
67
+ message:
68
+ 'Skip installing Magento and import database dump right away!'
69
+ }
70
+ ]
71
+ })
70
72
 
71
73
  if (doYouWantToRunSetupOnEmptyDB === 'skip') {
72
74
  subTask.skip()
@@ -180,16 +180,18 @@ const enableMagentoComposerPlugins = () => ({
180
180
  })
181
181
  }
182
182
 
183
- const answerForEnablingPlugins = await task.prompt({
184
- type: 'Select',
185
- message: `Composer 2.2 requires manually allowing composer-plugins to run.
183
+ const answerForEnablingPlugins = ctx.nonInteractive
184
+ ? 'all-individual'
185
+ : await task.prompt({
186
+ type: 'Select',
187
+ message: `Composer 2.2 requires manually allowing composer-plugins to run.
186
188
  Magento requires the following plugins to correctly operate:
187
189
 
188
190
  ${missingPluginsFromAllowPlugins.map((p) => logger.style.code(p)).join('\n')}
189
191
 
190
192
  Do you want to enable them all or disable some of them?`,
191
- choices: pluginOptions
192
- })
193
+ choices: pluginOptions
194
+ })
193
195
 
194
196
  switch (answerForEnablingPlugins.toLowerCase()) {
195
197
  case 'all': {
@@ -22,21 +22,23 @@ const indexProducts = () => ({
22
22
  ({ status }) => status !== 'valid'
23
23
  )
24
24
 
25
- const doYouWantToSkipIndexingPart = await task.prompt({
26
- type: 'Select',
27
- message: `Do you want to index the products? (There are ${invalidIndexers.length} invalid indexers, total indexers: ${data[0].length})\n`,
28
- choices: [
29
- {
30
- name: 'index',
31
- message: 'Yes, index them please'
32
- },
33
- {
34
- name: 'skip',
35
- message:
36
- 'Skip, do not index them. I will do it later myself'
37
- }
38
- ]
39
- })
25
+ const doYouWantToSkipIndexingPart = ctx.nonInteractive
26
+ ? 'index'
27
+ : await task.prompt({
28
+ type: 'Select',
29
+ message: `Do you want to index the products? (There are ${invalidIndexers.length} invalid indexers, total indexers: ${data[0].length})\n`,
30
+ choices: [
31
+ {
32
+ name: 'index',
33
+ message: 'Yes, index them please'
34
+ },
35
+ {
36
+ name: 'skip',
37
+ message:
38
+ 'Skip, do not index them. I will do it later myself'
39
+ }
40
+ ]
41
+ })
40
42
 
41
43
  if (doYouWantToSkipIndexingPart === 'skip') {
42
44
  task.skip()
@@ -51,26 +51,28 @@ const installMagento = ({ isDbEmpty = false } = {}) => ({
51
51
  response && response.length > 0 && response[0]
52
52
 
53
53
  if (usersWithUsernameAdmin && usersWithUsernameAdmin.length > 0) {
54
- const confirmDeleteAdminUsers = await task.prompt({
55
- type: 'Select',
56
- message: `In order to install Magento in database you will need to delete admin user with username ${logger.style.command(
57
- 'admin'
58
- )}`,
59
- choices: [
60
- {
61
- name: 'delete-all',
62
- message: `Delete all admin users (${logger.style.code(
63
- 'Recommended'
64
- )})`
65
- },
66
- {
67
- name: 'delete-only-admin',
68
- message: `Delete only admin user with ${logger.style.command(
69
- 'admin'
70
- )} username`
71
- }
72
- ]
73
- })
54
+ const confirmDeleteAdminUsers = ctx.nonInteractive
55
+ ? 'delete-all'
56
+ : await task.prompt({
57
+ type: 'Select',
58
+ message: `In order to install Magento in database you will need to delete admin user with username ${logger.style.command(
59
+ 'admin'
60
+ )}`,
61
+ choices: [
62
+ {
63
+ name: 'delete-all',
64
+ message: `Delete all admin users (${logger.style.code(
65
+ 'Recommended'
66
+ )})`
67
+ },
68
+ {
69
+ name: 'delete-only-admin',
70
+ message: `Delete only admin user with ${logger.style.command(
71
+ 'admin'
72
+ )} username`
73
+ }
74
+ ]
75
+ })
74
76
 
75
77
  await databaseConnection.query('SET FOREIGN_KEY_CHECKS = 0;')
76
78
 
@@ -183,7 +185,7 @@ const installMagento = ({ isDbEmpty = false } = {}) => ({
183
185
  --cache-backend='redis' \
184
186
  --cache-backend-redis-server='${hostMachine}' \
185
187
  --cache-backend-redis-port='${ports.redis}' \
186
- --cache-backend-redis-db='0't \
188
+ --cache-backend-redis-db='0' \
187
189
  --db-host='${hostMachine}:${ports.mariadb}' \
188
190
  --db-name='${defaultMagentoDatabase}' \
189
191
  --db-user='${defaultMagentoUser.user}' \
@@ -219,21 +221,23 @@ const installMagento = ({ isDbEmpty = false } = {}) => ({
219
221
  )
220
222
  )
221
223
  ) {
222
- const confirmToWipeEnvPhp = await task.prompt({
223
- type: 'Confirm',
224
- message: `We detected that your encryption key in ${logger.style.file(
225
- 'app/etc/env.php'
226
- )} file is not accepted by Magento installer.
224
+ const confirmToWipeEnvPhp = ctx.nonInteractive
225
+ ? true
226
+ : await task.prompt({
227
+ type: 'Confirm',
228
+ message: `We detected that your encryption key in ${logger.style.file(
229
+ 'app/etc/env.php'
230
+ )} file is not accepted by Magento installer.
227
231
  To fix this issue we will need to ${logger.style.misc(
228
- 'DELETE'
229
- )} ${logger.style.file(
230
- 'app/etc/env.php'
231
- )} file. It will be recreated but existing encryption key but if you any custom configuration in it will be lost.
232
+ 'DELETE'
233
+ )} ${logger.style.file(
234
+ 'app/etc/env.php'
235
+ )} file. It will be recreated but existing encryption key but if you any custom configuration in it will be lost.
232
236
 
233
237
  Without this you will not be able to install Magento at this moment.
234
238
 
235
239
  Do you want to continue?`
236
- })
240
+ })
237
241
 
238
242
  if (confirmToWipeEnvPhp) {
239
243
  try {
@@ -209,23 +209,25 @@ const checkComposerCredentials = () => ({
209
209
 
210
210
  if (composerAuthInRcFile) {
211
211
  doConfigure = false
212
- const loadCredentialsFrom = await task.prompt({
213
- type: 'Confirm',
214
- message: `We detected that you have ${logger.style.misc(
215
- 'COMPOSER_AUTH'
216
- )} environment variable set in ${logger.style.file(
217
- shellConfigFilePath
218
- )} file,
212
+ const loadCredentialsFrom = ctx.nonInteractive
213
+ ? true
214
+ : await task.prompt({
215
+ type: 'Confirm',
216
+ message: `We detected that you have ${logger.style.misc(
217
+ 'COMPOSER_AUTH'
218
+ )} environment variable set in ${logger.style.file(
219
+ shellConfigFilePath
220
+ )} file,
219
221
  but we do not see this variable inside ${logger.style.code(
220
- 'magento-scripts'
221
- )} process.
222
+ 'magento-scripts'
223
+ )} process.
222
224
 
223
225
  ${logger.style.misc(
224
226
  "! Don't forget to reload your shell after process is finished !"
225
227
  )}
226
228
 
227
229
  Would you like to load them now?`
228
- })
230
+ })
229
231
 
230
232
  if (loadCredentialsFrom) {
231
233
  const credentialsLine = lines.find((line) =>
@@ -249,6 +251,19 @@ Would you like to load them now?`
249
251
  }
250
252
 
251
253
  if (doConfigure) {
254
+ if (ctx.nonInteractive) {
255
+ throw new KnownError(
256
+ `Composer credentials are required but were not found in ${logger.style.misc(
257
+ '$COMPOSER_AUTH'
258
+ )} or ${logger.style.file('./auth.json')}.
259
+
260
+ Provide them non-interactively by setting the ${logger.style.misc(
261
+ '$COMPOSER_AUTH'
262
+ )} environment variable or by creating an ${logger.style.file(
263
+ './auth.json'
264
+ )} file, then run the command again.`
265
+ )
266
+ }
252
267
  return task.newListr(configureComposerCredentials())
253
268
  }
254
269
  }
@@ -306,20 +321,22 @@ Do you want to remove them now? File will be overwritten.`
306
321
  : null
307
322
 
308
323
  if (message) {
309
- const response = await task.prompt({
310
- message,
311
- type: 'Select',
312
- choices: [
313
- {
314
- name: 'overwrite',
315
- message: 'Yes, please!'
316
- },
317
- {
318
- name: 'skip',
319
- message: "No, I know what I'm doing"
320
- }
321
- ]
322
- })
324
+ const response = ctx.nonInteractive
325
+ ? 'skip'
326
+ : await task.prompt({
327
+ message,
328
+ type: 'Select',
329
+ choices: [
330
+ {
331
+ name: 'overwrite',
332
+ message: 'Yes, please!'
333
+ },
334
+ {
335
+ name: 'skip',
336
+ message: "No, I know what I'm doing"
337
+ }
338
+ ]
339
+ })
323
340
 
324
341
  if (response === 'overwrite') {
325
342
  if (repoMagentoCredentials.username) {
@@ -48,29 +48,31 @@ const checkDockerDesktopContext = () => ({
48
48
  return
49
49
  }
50
50
 
51
- const confirmContextChange = await task.prompt({
52
- type: 'Select',
53
- message: `Do you want to change current Docker Desktop context (${logger.style.code(
54
- currentlyUsedContext.Name
55
- )}) to ${logger.style.code('default')}?`,
56
- choices: [
57
- {
58
- name: 'yes',
59
- message: 'Yes'
60
- },
61
- {
62
- name: 'no',
63
- message:
64
- "No, I don't know what this means, but you can ask again on next start."
65
- },
66
- {
67
- name: 'skip',
51
+ const confirmContextChange = ctx.nonInteractive
52
+ ? 'no'
53
+ : await task.prompt({
54
+ type: 'Select',
55
+ message: `Do you want to change current Docker Desktop context (${logger.style.code(
56
+ currentlyUsedContext.Name
57
+ )}) to ${logger.style.code('default')}?`,
58
+ choices: [
59
+ {
60
+ name: 'yes',
61
+ message: 'Yes'
62
+ },
63
+ {
64
+ name: 'no',
65
+ message:
66
+ "No, I don't know what this means, but you can ask again on next start."
67
+ },
68
+ {
69
+ name: 'skip',
68
70
 
69
- message:
70
- "I do know what this means and I DON'T want to change context for Docker. Also, save this answer to never ask again."
71
- }
72
- ]
73
- })
71
+ message:
72
+ "I do know what this means and I DON'T want to change context for Docker. Also, save this answer to never ask again."
73
+ }
74
+ ]
75
+ })
74
76
 
75
77
  if (confirmContextChange === 'skip') {
76
78
  cmaGlobalConfig.set(
@@ -21,22 +21,24 @@ const checkDockerSocketPermissions = () => ({
21
21
  } catch (e) {
22
22
  // check for permission
23
23
  if (Math.abs(e.errno) === Math.abs(os.constants.errno.EACCES)) {
24
- const confirmPrompt = await task.prompt({
25
- type: 'Confirm',
26
- message: `We detected that your Docker socket, located in ${logger.style.file(
27
- dockerSocketPath
28
- )}, have permissions set, that prevents user (${logger.style.misc(
29
- os.userInfo().username
30
- )}) from accessing it.
24
+ const confirmPrompt = ctx.nonInteractive
25
+ ? false
26
+ : await task.prompt({
27
+ type: 'Confirm',
28
+ message: `We detected that your Docker socket, located in ${logger.style.file(
29
+ dockerSocketPath
30
+ )}, have permissions set, that prevents user (${logger.style.misc(
31
+ os.userInfo().username
32
+ )}) from accessing it.
31
33
 
32
34
  We can fix it by running the following command: ${logger.style.command(
33
- fixCommand
34
- )}
35
+ fixCommand
36
+ )}
35
37
 
36
38
  Would you like to fix this permission issue?
37
39
 
38
40
  Otherwise installation will likely fail.`
39
- })
41
+ })
40
42
 
41
43
  if (confirmPrompt) {
42
44
  return task.newListr(executeSudoCommand(fixCommand))
@@ -61,11 +61,13 @@ const checkDockerStatusMacOS = () => ({
61
61
  result.includes('Is the docker daemon running?') ||
62
62
  result.includes('docker: command not found')
63
63
  ) {
64
- const dockerOpenAppConfirmation = await task.prompt({
65
- type: 'Confirm',
66
- message:
67
- 'Looks like Docker is not running, would you like us to open a Docker for Mac application and wait for it to start up?'
68
- })
64
+ const dockerOpenAppConfirmation = ctx.nonInteractive
65
+ ? false
66
+ : await task.prompt({
67
+ type: 'Confirm',
68
+ message:
69
+ 'Looks like Docker is not running, would you like us to open a Docker for Mac application and wait for it to start up?'
70
+ })
69
71
 
70
72
  if (
71
73
  dockerOpenAppConfirmation &&
@@ -136,12 +138,14 @@ const checkDockerStatusLinux = () => ({
136
138
 
137
139
  if (engine.exists) {
138
140
  if (!engine.isEnabled && !engine.isRunning) {
139
- const dockerStartConfirmation = await task.prompt({
140
- type: 'Confirm',
141
- message: `Looks like Docker Engine is not enabled and not running, would you like to enable and run it?
141
+ const dockerStartConfirmation = ctx.nonInteractive
142
+ ? false
143
+ : await task.prompt({
144
+ type: 'Confirm',
145
+ message: `Looks like Docker Engine is not enabled and not running, would you like to enable and run it?
142
146
 
143
147
  This action requires root privileges.`
144
- })
148
+ })
145
149
 
146
150
  if (dockerStartConfirmation) {
147
151
  await engine.service.enableAndStart()
@@ -150,12 +154,14 @@ const checkDockerStatusLinux = () => ({
150
154
  }
151
155
  task.skip('User skipped running Docker')
152
156
  } else if (!engine.isRunning) {
153
- const dockerStartConfirmation = await task.prompt({
154
- type: 'Confirm',
155
- message: `Looks like Docker Engine is not running, would you like to run it?
157
+ const dockerStartConfirmation = ctx.nonInteractive
158
+ ? false
159
+ : await task.prompt({
160
+ type: 'Confirm',
161
+ message: `Looks like Docker Engine is not running, would you like to run it?
156
162
 
157
163
  This action requires root privileges.`
158
- })
164
+ })
159
165
 
160
166
  if (dockerStartConfirmation) {
161
167
  await engine.service.start()
@@ -166,12 +172,14 @@ const checkDockerStatusLinux = () => ({
166
172
  }
167
173
  } else if (desktop.exists) {
168
174
  if (!desktop.isEnabled && !desktop.isRunning) {
169
- const dockerStartConfirmation = await task.prompt({
170
- type: 'Confirm',
171
- message: `Looks like Docker Desktop is not enabled and not running, would you like to enable and run it?
175
+ const dockerStartConfirmation = ctx.nonInteractive
176
+ ? false
177
+ : await task.prompt({
178
+ type: 'Confirm',
179
+ message: `Looks like Docker Desktop is not enabled and not running, would you like to enable and run it?
172
180
 
173
181
  This action requires root privileges.`
174
- })
182
+ })
175
183
 
176
184
  if (dockerStartConfirmation) {
177
185
  await desktop.service.enableAndStart()
@@ -180,12 +188,14 @@ const checkDockerStatusLinux = () => ({
180
188
  }
181
189
  task.skip('User skipped running Docker')
182
190
  } else if (!desktop.isRunning) {
183
- const dockerStartConfirmation = await task.prompt({
184
- type: 'Confirm',
185
- message: `Looks like Docker Desktop is not running, would you like to run it?
191
+ const dockerStartConfirmation = ctx.nonInteractive
192
+ ? false
193
+ : await task.prompt({
194
+ type: 'Confirm',
195
+ message: `Looks like Docker Desktop is not running, would you like to run it?
186
196
 
187
197
  This action requires root privileges.`
188
- })
198
+ })
189
199
 
190
200
  if (dockerStartConfirmation) {
191
201
  await desktop.service.start()
@@ -176,7 +176,7 @@ const start = () => ({
176
176
  {
177
177
  title: 'Opening browser',
178
178
  skip: async (ctx) => {
179
- if (ctx.noOpen) {
179
+ if (ctx.open === false) {
180
180
  return true
181
181
  }
182
182
 
@@ -0,0 +1,79 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+
4
+ const AGENTS_MD_CONTENT = `# CMA (magento-scripts) — AI/CI Reference
5
+
6
+ > Auto-generated by magento-scripts. Do not delete.
7
+
8
+ ## Critical
9
+
10
+ - **Cannot run as root** — exits immediately with code 1, no override.
11
+ - **Non-TTY safe** — silent renderer activates automatically in CI/pipes; no \`-q\` needed.
12
+ - **Do NOT use \`--\` with exec** — it gets passed as the command and fails. Correct: \`magento-scripts exec php bin/magento cache:flush\`
13
+ - **\`cli\` is TTY-only** — use \`exec php bin/magento <cmd>\` in automation instead.
14
+ - **\`import-db\` is self-contained** — it stops running containers, assigns ports, starts services, waits for MariaDB, and imports. Do NOT run \`start\` before or between \`import-db\` attempts — that creates port conflicts. Just run \`import-db\` directly.
15
+ - **Do NOT run \`start\` then \`import-db\`** — each command manages its own container lifecycle. Running both creates split-brain port assignments. Use one or the other.
16
+ - **Long-running commands** — \`start\` and \`import-db\` can take 10+ minutes (container setup, large dumps). Set timeouts to at least 600000ms (10 min) or run without a timeout.
17
+ - **Shell escaping** — Avoid \`!\` in SQL strings passed via \`exec\` (e.g., \`!=\`), as the shell may interpret it. Use SQL alternatives like \`<>\` instead of \`!=\`.
18
+
19
+ ## Commands
20
+
21
+ | Command | What it does | Key flags |
22
+ |---------|-------------|-----------|
23
+ | \`start\` | Start Docker environment | \`--no-open\`, \`--skip-setup\`, \`--port\` |
24
+ | \`stop\` | Stop all containers | — |
25
+ | \`status\` | Show container/DB status | — |
26
+ | \`exec <container> [cmd...]\` | Run command in container | use \`--\` before flags |
27
+ | \`import-db [file]\` | Import SQL dump into MariaDB | \`-y\` (non-interactive), \`--remote-db=ssh://user@host\` |
28
+ | \`logs <container>\` | Stream container logs | \`--tail N\`, \`--follow\` |
29
+ | \`cleanup\` | Remove cached/generated files | \`--force\` |
30
+ | \`cli\` | Interactive shell (TTY only) | — |
31
+ | \`link <path>\` | Link ScandiPWA theme | — |
32
+
33
+ ## Containers
34
+
35
+ \`php\`, \`phpWithXdebug\`, \`nginx\`, \`sslTerminator\`, \`redis\`, \`mariadb\`, \`elasticsearch\`, \`maildev\`, \`varnish\` (if enabled)
36
+
37
+ ## Examples
38
+
39
+ \`\`\`bash
40
+ magento-scripts start --no-open --skip-setup
41
+ magento-scripts import-db dump.sql -y
42
+ magento-scripts import-db -y --remote-db=ssh://user@host
43
+ magento-scripts exec php bin/magento cache:flush
44
+ magento-scripts exec php bin/magento indexer:reindex
45
+
46
+ # Query MariaDB (use mariadb binary, not mysql)
47
+ magento-scripts exec mariadb mariadb -u magento -pmagento magento -e "SELECT COUNT(*) FROM store"
48
+ magento-scripts logs magento --tail 100
49
+ magento-scripts stop
50
+ \`\`\`
51
+ `
52
+
53
+ /**
54
+ * Write AGENTS.md and CLAUDE.md to the current working directory if they do not already exist.
55
+ * This gives AI agents and CI pipelines a command reference for the project.
56
+ */
57
+ function ensureAgentsMd() {
58
+ const cwd = process.cwd()
59
+
60
+ const agentsDest = path.join(cwd, 'AGENTS.md')
61
+ if (!fs.existsSync(agentsDest)) {
62
+ try {
63
+ fs.writeFileSync(agentsDest, AGENTS_MD_CONTENT, 'utf8')
64
+ } catch (e) {
65
+ // Non-fatal — silently skip if the directory is not writable
66
+ }
67
+ }
68
+
69
+ const claudeDest = path.join(cwd, 'CLAUDE.md')
70
+ if (!fs.existsSync(claudeDest)) {
71
+ try {
72
+ fs.writeFileSync(claudeDest, '@AGENTS.md\n', 'utf8')
73
+ } catch (e) {
74
+ // Non-fatal
75
+ }
76
+ }
77
+ }
78
+
79
+ module.exports = ensureAgentsMd
@@ -30,18 +30,16 @@ const joinCommandArgs = (...args) =>
30
30
  * @param {{ containerName: string, commands: string[], user?: string, env?: Record<string, string> }} param0
31
31
  */
32
32
  const executeInContainer = ({ containerName, commands, user, env }) => {
33
- if (!process.stdin.isTTY) {
34
- process.stderr.write('This app works only in TTY mode')
35
- process.exit(1)
36
- }
37
33
  const [commandBin, ...commandsArgs] = commands
38
34
 
35
+ const isTTY = process.stdin.isTTY
36
+
39
37
  const execArgs = execCommand({
40
38
  container: containerName,
41
39
  command: commandBin,
42
40
  user,
43
- tty: true,
44
- interactive: true,
41
+ tty: isTTY,
42
+ interactive: isTTY,
45
43
  env: env || {}
46
44
  })
47
45
  const [command, ...args] = execArgs
@@ -91,18 +89,14 @@ const executeInContainerNonInteractive = async ({
91
89
  * @param {string[]} commands
92
90
  */
93
91
  const runInContainer = async (options, commands) => {
94
- if (!process.stdin.isTTY) {
95
- process.stderr.write('This app works only in TTY mode')
96
- process.exit(1)
97
- }
98
-
92
+ const isTTY = process.stdin.isTTY
99
93
  const [commandBin, ...commandsArgs] = commands
100
94
 
101
95
  const runResult = await run(
102
96
  {
103
97
  ...options,
104
98
  command: joinCommandArgs(commandBin, ...commandsArgs),
105
- tty: true,
99
+ tty: isTTY,
106
100
  detach: false,
107
101
  rm: true
108
102
  },
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Scripts and configuration used by CMA.",
4
4
  "homepage": "https://docs.create-magento-app.com/",
5
5
  "repository": "github:scandipwa/create-magento-app",
6
- "version": "2.4.11",
6
+ "version": "2.4.12-alpha.0",
7
7
  "main": "./index.js",
8
8
  "types": "./typings/index.d.ts",
9
9
  "license": "OSL-3.0",
@@ -59,5 +59,5 @@
59
59
  "@types/node": "^20.14.11",
60
60
  "@types/yargs": "^17.0.32"
61
61
  },
62
- "gitHead": "64c177af194cf32de4d8ae08116f016d30ec7f01"
62
+ "gitHead": "0fe2409c4e619ff985cf90a7f3d6cc4d45ca2552"
63
63
  }
@@ -8,6 +8,7 @@ import { PHPStormConfig } from './phpstorm'
8
8
 
9
9
  export interface ListrContext {
10
10
  noOpen?: boolean
11
+ open?: boolean
11
12
  skipSetup?: boolean
12
13
  resetGlobalConfig?: boolean
13
14
  withCustomersData?: boolean
@@ -159,4 +160,14 @@ export interface ListrContext {
159
160
  dockerClientData?: DockerVersionResult['Client']
160
161
  dockerVersion?: DockerVersionResult['Server']['Version']
161
162
  dockerMemoryLimit: number
163
+ silent?: boolean
164
+ nonInteractive?: boolean
165
+ deleteDb?: 'always' | 'never' | 'ask'
166
+ dbUser?: 'root' | 'magento'
167
+ fixCollation?: 'auto' | 'never' | 'ask'
168
+ installMagentoEmptyDb?: 'yes' | 'no' | 'ask'
169
+ privateKey?: string
170
+ passphrase?: string
171
+ makeRemoteDumps?: boolean
172
+ remoteDumpCommand?: string
162
173
  }