@joltdesign/scripts 0.21.0 → 0.22.1

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.
@@ -1,8 +1,10 @@
1
+ import fs from 'node:fs/promises';
1
2
  import { userInfo } from 'node:os';
3
+ import path from 'node:path';
2
4
  import ansis from 'ansis';
3
5
  import { Option } from 'clipanion';
4
6
  import * as t from 'typanion';
5
- import { execC, which } from '../utils.js';
7
+ import { execC, fileExists, which } from '../utils.js';
6
8
  import JoltCommand from './JoltCommand.js';
7
9
  // Possible sub-arguments for the CLI command as of WP-CLI v2.12.0
8
10
  const possibleCliArgs = [
@@ -113,9 +115,10 @@ export class WPUpdateCommand extends JoltCommand {
113
115
  this.skipCore = Option.Boolean('--skip-core', false, { description: 'Skip WordPress core updates' });
114
116
  this.skipPlugins = Option.Boolean('--skip-plugins', false, { description: 'Skip plugin updates' });
115
117
  this.skipThemes = Option.Boolean('--skip-themes', false, { description: 'Skip theme updates' });
118
+ this.skipLanguages = Option.Boolean('--skip-languages', false, { description: 'Skip language/translation updates' });
116
119
  }
117
120
  async command() {
118
- const { config, context: { stdout, stderr }, skipCore, skipPlugins, skipThemes, logo, } = this;
121
+ const { config, context: { stdout, stderr }, skipCore, skipPlugins, skipThemes, skipLanguages, logo, } = this;
119
122
  stdout.write(ansis.bold(`${logo} WordPress Updates\n\n`));
120
123
  // Load configuration
121
124
  const wpConfig = await config.loadWordPressConfig();
@@ -127,30 +130,19 @@ export class WPUpdateCommand extends JoltCommand {
127
130
  let updatedPluginCount = 0;
128
131
  let updatedThemeCount = 0;
129
132
  let updatedCore = false;
133
+ // Track detailed update information for summary
134
+ const updateSummary = {
135
+ plugins: [],
136
+ themes: [],
137
+ core: null,
138
+ translations: false,
139
+ };
130
140
  // Get current status
131
141
  if (!skipPlugins) {
132
- stdout.write(ansis.cyan('🔌 Checking plugins...\n'));
133
- const yarnCommand = await config.command('yarn');
134
- const pluginsResult = await execC(yarnCommand, ['jolt', 'wp', 'cli', 'plugin', 'list', '--json'], {
135
- reject: false,
136
- });
137
- const pluginsJson = pluginsResult.exitCode === 0 ? String(pluginsResult.stdout || '') : null;
138
- if (pluginsJson) {
139
- const plugins = this.parsePluginJson(pluginsJson);
140
- stdout.write(`Found ${plugins.length} plugins\n`);
141
- }
142
+ await this.getItems('plugin');
142
143
  }
143
144
  if (!skipThemes) {
144
- stdout.write(ansis.cyan('🎨 Checking themes...\n'));
145
- const yarnCommand = await config.command('yarn');
146
- const themesResult = await execC(yarnCommand, ['jolt', 'wp', 'cli', 'theme', 'list', '--json'], {
147
- reject: false,
148
- });
149
- const themesJson = themesResult.exitCode === 0 ? String(themesResult.stdout || '') : null;
150
- if (themesJson) {
151
- const themes = this.parseThemeJson(themesJson);
152
- stdout.write(`Found ${themes.length} themes\n`);
153
- }
145
+ await this.getItems('theme');
154
146
  }
155
147
  // We'll create the update branch only when we need to make our first commit
156
148
  const branchRef = { branch: undefined, created: false };
@@ -158,45 +150,21 @@ export class WPUpdateCommand extends JoltCommand {
158
150
  const originalHookPath = await this.disableGitHooks();
159
151
  try {
160
152
  // Update plugins
161
- if (!skipPlugins) {
162
- stdout.write(ansis.cyan('🔌 Updating plugins...\n'));
163
- const yarnCommand = await config.command('yarn');
164
- const pluginsResult = await execC(yarnCommand, ['jolt', 'wp', 'cli', 'plugin', 'list', '--json'], {
165
- reject: false,
166
- });
167
- const pluginsJson = pluginsResult.exitCode === 0 ? String(pluginsResult.stdout || '') : null;
168
- if (pluginsJson) {
169
- const plugins = this.parsePluginJson(pluginsJson);
170
- for (const plugin of plugins) {
171
- const didUpdate = await this.maybeUpdatePlugin(plugin, wpConfig, branchRef);
172
- if (didUpdate) {
173
- updatedPluginCount++;
174
- }
175
- }
176
- }
177
- }
153
+ const pluginUpdates = await this.processItemUpdates('plugin', skipPlugins, wpConfig, branchRef);
154
+ updatedPluginCount = pluginUpdates.count;
155
+ updateSummary.plugins = pluginUpdates.details;
178
156
  // Update themes
179
- if (!skipThemes) {
180
- stdout.write(ansis.cyan('🎨 Updating themes...\n'));
181
- const yarnCommand = await config.command('yarn');
182
- const themesResult = await execC(yarnCommand, ['jolt', 'wp', 'cli', 'theme', 'list', '--json'], {
183
- reject: false,
184
- });
185
- const themesJson = themesResult.exitCode === 0 ? String(themesResult.stdout || '') : null;
186
- if (themesJson) {
187
- const themes = this.parseThemeJson(themesJson);
188
- for (const theme of themes) {
189
- const didUpdate = await this.maybeUpdateTheme(theme, wpConfig, branchRef);
190
- if (didUpdate) {
191
- updatedThemeCount++;
192
- }
193
- }
194
- }
195
- }
157
+ const themeUpdates = await this.processItemUpdates('theme', skipThemes, wpConfig, branchRef);
158
+ updatedThemeCount = themeUpdates.count;
159
+ updateSummary.themes = themeUpdates.details;
196
160
  // Update core
197
161
  if (!skipCore) {
198
162
  stdout.write(ansis.cyan('📦 Checking WordPress core...\n'));
199
- updatedCore = await this.maybeUpdateCore(wpConfig, branchRef);
163
+ const coreResult = await this.maybeUpdateCore(wpConfig, branchRef);
164
+ updatedCore = coreResult.updated;
165
+ if (coreResult.updated && coreResult.details) {
166
+ updateSummary.core = coreResult.details;
167
+ }
200
168
  }
201
169
  }
202
170
  finally {
@@ -210,218 +178,280 @@ export class WPUpdateCommand extends JoltCommand {
210
178
  stdout.write(ansis.cyan('📦 Updated WordPress core\n'));
211
179
  }
212
180
  const totalUpdates = updatedPluginCount + updatedThemeCount + (updatedCore ? 1 : 0);
181
+ // Update translations if possible
182
+ let updatedTranslations = false;
183
+ if (!skipLanguages && (totalUpdates > 0 || !branchRef.created)) {
184
+ stdout.write(ansis.cyan('🌐 Updating translations...\n'));
185
+ updatedTranslations = await this.maybeUpdateTranslations(branchRef);
186
+ updateSummary.translations = updatedTranslations;
187
+ }
188
+ // Show detailed update summary
189
+ if (totalUpdates > 0 || updatedTranslations) {
190
+ stdout.write(ansis.bold('\n📋 Update Summary:\n'));
191
+ if (updateSummary.plugins.length > 0) {
192
+ stdout.write(ansis.green('🔌 Plugins updated:\n'));
193
+ for (const plugin of updateSummary.plugins) {
194
+ stdout.write(ansis.cyan(` • ${plugin.title} (${plugin.fromVersion} → ${plugin.toVersion})\n`));
195
+ }
196
+ }
197
+ if (updateSummary.themes.length > 0) {
198
+ stdout.write(ansis.green('🎨 Themes updated:\n'));
199
+ for (const theme of updateSummary.themes) {
200
+ stdout.write(ansis.cyan(` • ${theme.title} (${theme.fromVersion} → ${theme.toVersion})\n`));
201
+ }
202
+ }
203
+ if (updateSummary.core) {
204
+ stdout.write(ansis.green('📦 WordPress core updated:\n'));
205
+ stdout.write(ansis.cyan(` • WordPress (${updateSummary.core.fromVersion} → ${updateSummary.core.toVersion})\n`));
206
+ }
207
+ if (updateSummary.translations) {
208
+ stdout.write(ansis.green('🌐 Translations updated\n'));
209
+ }
210
+ }
213
211
  if (totalUpdates > 0 && branchRef.created) {
214
212
  stdout.write(ansis.yellow('\nNext steps:\n'));
215
- stdout.write(`• Review updates: ${ansis.dim('jolt wp update modify')}\n`);
213
+ stdout.write(`• Review updates: ${ansis.dim(await this.getUpdateCommand('modify'))}\n`);
216
214
  // Use root config value directly
217
- stdout.write(`• Merge to ${await config.get('branch')}: ${ansis.dim('jolt wp update merge')}\n`);
215
+ stdout.write(`• Merge to ${await config.get('branch')}: ${ansis.dim(await this.getUpdateCommand('merge'))}\n`);
218
216
  }
219
- else if (totalUpdates === 0) {
217
+ else if (totalUpdates === 0 && !updatedTranslations) {
220
218
  stdout.write(ansis.green('\n✅ No updates available - staying on current branch\n'));
221
219
  }
222
220
  return 0;
223
221
  }
224
- // Note: WP CLI invocations run via `yarn jolt wp cli ...` using execC directly.
225
- parsePluginJson(pluginsJson) {
226
- // Trim off any preceding warnings, try to look for the start of the actual JSON.
227
- const trimmed = pluginsJson.substring(pluginsJson.indexOf('[{'));
228
- const allPlugins = JSON.parse(trimmed);
229
- return allPlugins.filter((plugin) => !['dropin', 'must-use'].includes(plugin.status));
230
- }
231
- parseThemeJson(themeJson) {
232
- // Trim off any preceding warnings, try to look for the start of the actual JSON.
233
- const trimmed = themeJson.substring(themeJson.indexOf('[{'));
234
- return JSON.parse(trimmed);
235
- }
236
- // (No helper) obtain the package runner directly via config.command('yarn') where needed
237
- async createBranch() {
238
- const { config, context } = this;
239
- const isoDate = new Date().toISOString().replace(/[:.]/g, '-').replace(/T/, '_').slice(0, -5);
240
- const branchName = `joltWpUpdate/${isoDate}`;
241
- const gitCommand = await config.command('git');
242
- await execC(gitCommand, ['checkout', '-b', branchName], { context });
243
- return branchName;
244
- }
245
- async ensureBranchCreated(_wpConfig, branchRef) {
246
- if (!branchRef.created) {
247
- branchRef.branch = await this.createBranch();
248
- branchRef.created = true;
249
- const { context: { stdout }, } = this;
250
- stdout.write(ansis.green(`📋 Created update branch ${branchRef.branch}\n`));
251
- }
252
- return branchRef.branch;
253
- }
254
- async disableGitHooks() {
255
- const { config, context } = this;
256
- const gitCommand = await config.command('git');
257
- try {
258
- const result = await execC(gitCommand, ['config', '--get', 'core.hooksPath'], { context, reject: false });
259
- const originalHookPath = String(result.stdout || '').trim();
260
- await execC(gitCommand, ['config', 'core.hooksPath', '/dev/null'], { context });
261
- return originalHookPath;
262
- }
263
- catch (_a) {
264
- await execC(gitCommand, ['config', 'core.hooksPath', '/dev/null'], { context });
265
- return '';
222
+ // Helper method to get the appropriate command format (short or long form)
223
+ async getUpdateCommand(subCommand) {
224
+ var _a;
225
+ const { config } = this;
226
+ const yarnCommand = await config.command('yarn');
227
+ const packageJson = await config.getPackageJson();
228
+ // Check if there's an 'update' script that acts as a shortcut for 'jolt wp update'
229
+ const updateScript = (_a = packageJson === null || packageJson === void 0 ? void 0 : packageJson.scripts) === null || _a === void 0 ? void 0 : _a.update;
230
+ if (updateScript && (updateScript === 'jolt wp update' || updateScript.startsWith('jolt wp update '))) {
231
+ return `${yarnCommand} update ${subCommand}`;
266
232
  }
233
+ // Fall back to the full command
234
+ return `${yarnCommand} jolt wp update ${subCommand}`;
267
235
  }
268
- async rollbackGitHooks(originalHookPath) {
236
+ // Helper method to execute WP CLI commands
237
+ async executeWpCli(args, options) {
269
238
  const { config, context } = this;
270
- const gitCommand = await config.command('git');
271
- if (originalHookPath) {
272
- await execC(gitCommand, ['config', 'core.hooksPath', originalHookPath], { context });
273
- }
274
- else {
275
- await execC(gitCommand, ['config', '--unset', 'core.hooksPath'], { context, reject: false });
276
- }
277
- }
278
- async maybeUpdatePlugin(plugin, wpConfig, branchRef) {
239
+ const yarnCommand = await config.command('yarn');
240
+ // For silent operations, don't pass context to suppress output
241
+ const execOptions = {
242
+ reject: false,
243
+ ...options,
244
+ ...(!(options === null || options === void 0 ? void 0 : options.silent) ? { context } : {}),
245
+ };
246
+ const result = await execC(yarnCommand, ['jolt', 'wp', 'cli', ...args], execOptions);
247
+ return {
248
+ exitCode: result.exitCode || 0,
249
+ stdout: result.exitCode === 0 ? String(result.stdout || '') : null,
250
+ };
251
+ }
252
+ // Configuration for different item types
253
+ getItemConfig(type) {
254
+ const configs = {
255
+ plugin: {
256
+ type: 'plugin',
257
+ icon: '🔌',
258
+ listCommand: ['plugin', 'list', '--json'],
259
+ updateCommand: (name) => ['plugin', 'update', name],
260
+ getFolder: (wpConfig) => wpConfig.pluginFolder,
261
+ commitPrefix: 'Update',
262
+ },
263
+ theme: {
264
+ type: 'theme',
265
+ icon: '🎨',
266
+ listCommand: ['theme', 'list', '--json'],
267
+ updateCommand: (name) => ['theme', 'update', name],
268
+ getFolder: (wpConfig) => wpConfig.themeFolder,
269
+ commitPrefix: 'Update theme',
270
+ },
271
+ };
272
+ return configs[type];
273
+ }
274
+ // Generic method to get and parse item lists
275
+ async getItems(type) {
279
276
  const { context: { stdout }, } = this;
280
- if (wpConfig.doNotUpdate.includes(plugin.name)) {
281
- stdout.write(ansis.dim(` Skipping ${plugin.name} (configured to skip)\n`));
282
- return false;
283
- }
284
- stdout.write(` Checking ${plugin.name}...`);
285
- if (plugin.update === 'available') {
286
- stdout.write(ansis.green(' updating\n'));
287
- return await this.updatePlugin(plugin, wpConfig, branchRef);
288
- }
289
- if (plugin.update === 'none') {
290
- stdout.write(ansis.dim(' up to date\n'));
291
- }
292
- else if (plugin.update === 'version higher than expected') {
293
- stdout.write(ansis.yellow(` local version ${plugin.version} is higher than remote\n`));
294
- }
295
- else {
296
- stdout.write(ansis.red(` unknown status: ${plugin.update}\n`));
277
+ const itemConfig = this.getItemConfig(type);
278
+ stdout.write(ansis.cyan(`${itemConfig.icon} Checking ${type}s...\n`));
279
+ const result = await this.executeWpCli(itemConfig.listCommand, { silent: true });
280
+ if (!result.stdout) {
281
+ return [];
282
+ }
283
+ const items = this.parseItemJson(result.stdout);
284
+ stdout.write(`Found ${items.length} ${type}s\n`);
285
+ return items;
286
+ }
287
+ // Generic method to parse item JSON (plugins/themes have same structure)
288
+ parseItemJson(itemJson) {
289
+ // Trim off any preceding warnings, try to look for the start of the actual JSON.
290
+ const trimmed = itemJson.substring(itemJson.indexOf('[{'));
291
+ const allItems = JSON.parse(trimmed);
292
+ // For plugins, filter out dropin and must-use plugins
293
+ return allItems.filter((item) => !['dropin', 'must-use'].includes(item.status));
294
+ }
295
+ // Generic method to handle updating items
296
+ async processItemUpdates(type, skip, wpConfig, branchRef) {
297
+ if (skip) {
298
+ return { count: 0, details: [] };
299
+ }
300
+ const itemConfig = this.getItemConfig(type);
301
+ const items = await this.getItems(type);
302
+ const updateDetails = [];
303
+ let count = 0;
304
+ if (items.length > 0) {
305
+ this.context.stdout.write(ansis.cyan(`${itemConfig.icon} Updating ${type}s...\n`));
306
+ for (const item of items) {
307
+ const result = await this.maybeUpdateItem(item, wpConfig, branchRef, itemConfig);
308
+ if (result.updated) {
309
+ count++;
310
+ if (result.details) {
311
+ updateDetails.push(result.details);
312
+ }
313
+ }
314
+ }
297
315
  }
298
- return false;
316
+ return { count, details: updateDetails };
299
317
  }
300
- async maybeUpdateTheme(theme, wpConfig, branchRef) {
318
+ // Generic method to check and maybe update an item (plugin/theme)
319
+ async maybeUpdateItem(item, wpConfig, branchRef, itemConfig) {
301
320
  const { context: { stdout }, } = this;
302
- if (wpConfig.doNotUpdate.includes(theme.name)) {
303
- stdout.write(ansis.dim(` Skipping ${theme.name} (configured to skip)\n`));
304
- return false;
321
+ if (wpConfig.doNotUpdate.includes(item.name)) {
322
+ stdout.write(ansis.dim(` Skipping ${item.name} (configured to skip)\n`));
323
+ return { updated: false };
305
324
  }
306
- stdout.write(` Checking ${theme.name}...`);
307
- if (theme.update === 'available') {
325
+ stdout.write(` Checking ${item.name}...`);
326
+ if (item.update === 'available') {
308
327
  stdout.write(ansis.green(' updating\n'));
309
- return await this.updateTheme(theme, wpConfig, branchRef);
328
+ return await this.updateItem(item, wpConfig, branchRef, itemConfig);
310
329
  }
311
- if (theme.update === 'none') {
330
+ if (item.update === 'none') {
312
331
  stdout.write(ansis.dim(' up to date\n'));
313
332
  }
314
- else if (theme.update === 'version higher than expected') {
315
- stdout.write(ansis.yellow(` local version ${theme.version} is higher than remote\n`));
333
+ else if (item.update === 'version higher than expected') {
334
+ stdout.write(ansis.yellow(` local version ${item.version} is higher than remote\n`));
316
335
  }
317
336
  else {
318
- stdout.write(ansis.red(` unknown status: ${theme.update}\n`));
337
+ stdout.write(ansis.red(` unknown status: ${item.update}\n`));
319
338
  }
320
- return false;
339
+ return { updated: false };
321
340
  }
322
- async updatePlugin(plugin, wpConfig, branchRef) {
323
- const { config, context, context: { stdout, stderr }, } = this;
341
+ // Generic method to update an item (plugin/theme)
342
+ async updateItem(item, wpConfig, branchRef, itemConfig) {
343
+ const { config, context: { stdout, stderr }, } = this;
324
344
  try {
325
345
  const gitCommand = await config.command('git');
326
- const details = await this.getPluginDetails(plugin.name);
346
+ const details = await this.getDetails(item.name, itemConfig.type);
327
347
  if (!details) {
328
- return false;
348
+ return { updated: false };
329
349
  }
330
350
  const fromVersion = details.version;
331
351
  const prettyTitle = this.cleanTitle(details.title);
332
- const location = `${wpConfig.pluginFolder}/${plugin.name}`;
333
- const yarnCommand = await config.command('yarn');
334
- const updateResult = await execC(yarnCommand, ['jolt', 'wp', 'cli', 'plugin', 'update', plugin.name], {
335
- reject: false,
336
- });
337
- const updateResultStr = updateResult.exitCode === 0 ? String(updateResult.stdout || '') : null;
338
- if (!updateResultStr) {
339
- stderr.write(ansis.red(` Error updating ${plugin.name}\n`));
340
- return false;
352
+ const location = `${itemConfig.getFolder(wpConfig)}/${item.name}`;
353
+ const updateResult = await this.executeWpCli(itemConfig.updateCommand(item.name), { silent: true });
354
+ if (updateResult.exitCode !== 0) {
355
+ stderr.write(ansis.red(` Error updating ${item.name}\n`));
356
+ return { updated: false };
341
357
  }
342
- const newDetails = await this.getPluginDetails(plugin.name);
358
+ const newDetails = await this.getDetails(item.name, itemConfig.type);
343
359
  if (!newDetails || newDetails.version === details.version) {
344
360
  stderr.write(ansis.red(' Update failed!\n'));
345
- return false;
361
+ return { updated: false };
346
362
  }
347
363
  // Ensure branch is created before making our first commit
348
364
  await this.ensureBranchCreated(wpConfig, branchRef);
349
- const commitMessage = this.sanitizeCommitMessage(`Update ${prettyTitle} to ${newDetails.version}`);
350
- await execC(gitCommand, ['add', location], { context });
365
+ // Special case handling for Redis dropin
366
+ try {
367
+ const looksLikeRedis = /redis/i.test(String(item.name || details.title || ''));
368
+ if (looksLikeRedis) {
369
+ const cliResult = await this.executeWpCli(['redis', 'update-dropin'], { silent: true });
370
+ const wpContentDir = path.join(wpConfig.wpRoot, 'wp-content');
371
+ const dropinDest = path.join(wpContentDir, 'object-cache.php');
372
+ if (cliResult.exitCode === 0) {
373
+ const gitCommand = await config.command('git');
374
+ await execC(gitCommand, ['add', dropinDest]);
375
+ }
376
+ else {
377
+ // WP-CLI failed - fall back to copying the plugin-provided dropin
378
+ await this.updateRedisDropinFromPlugin(item, wpConfig, itemConfig);
379
+ }
380
+ }
381
+ }
382
+ catch (_a) {
383
+ // non-fatal - continue
384
+ }
385
+ const commitMessage = this.sanitizeCommitMessage(`${itemConfig.commitPrefix} ${prettyTitle} to ${newDetails.version}`);
386
+ await execC(gitCommand, ['add', location]);
351
387
  await execC(gitCommand, ['commit', '-m', commitMessage], {
352
- context,
353
388
  shell: false,
354
389
  env: { SKIP: 'prepare-commit-msg' },
355
390
  });
356
391
  stdout.write(ansis.green(` Updated ${prettyTitle} from ${fromVersion} to ${newDetails.version}\n`));
357
- return true;
392
+ return {
393
+ updated: true,
394
+ details: {
395
+ name: item.name,
396
+ title: prettyTitle,
397
+ fromVersion,
398
+ toVersion: newDetails.version,
399
+ },
400
+ };
358
401
  }
359
402
  catch (error) {
360
- stderr.write(ansis.red(` Error updating ${plugin.name}: ${error}\n`));
361
- return false;
403
+ stderr.write(ansis.red(` Error updating ${item.name}: ${error}\n`));
404
+ return { updated: false };
405
+ }
406
+ }
407
+ async createBranch() {
408
+ const { config } = this;
409
+ const isoDate = new Date().toISOString().replace(/[:.]/g, '-').replace(/T/, '_').slice(0, -5);
410
+ const branchName = `joltWpUpdate/${isoDate}`;
411
+ const gitCommand = await config.command('git');
412
+ await execC(gitCommand, ['checkout', '-b', branchName]);
413
+ return branchName;
414
+ }
415
+ async ensureBranchCreated(_wpConfig, branchRef) {
416
+ if (!branchRef.created) {
417
+ branchRef.branch = await this.createBranch();
418
+ branchRef.created = true;
419
+ const { context: { stdout }, } = this;
420
+ stdout.write(ansis.green(`📋 Created update branch ${branchRef.branch}\n`));
362
421
  }
422
+ return branchRef.branch;
363
423
  }
364
- async updateTheme(theme, wpConfig, branchRef) {
365
- const { config, context, context: { stdout, stderr }, } = this;
424
+ async disableGitHooks() {
425
+ const { config, context, context: { stderr }, } = this;
426
+ const gitCommand = await config.command('git');
366
427
  try {
367
- const gitCommand = await config.command('git');
368
- const details = await this.getThemeDetails(theme.name);
369
- if (!details) {
370
- return false;
371
- }
372
- const fromVersion = details.version;
373
- const prettyTitle = this.cleanTitle(details.title);
374
- const location = `${wpConfig.themeFolder}/${theme.name}`;
375
- const yarnCommand = await config.command('yarn');
376
- const updateResult = await execC(yarnCommand, ['jolt', 'wp', 'cli', 'theme', 'update', theme.name], {
377
- reject: false,
378
- });
379
- const updateResultStr = updateResult.exitCode === 0 ? String(updateResult.stdout || '') : null;
380
- if (!updateResultStr) {
381
- stderr.write(ansis.red(` Error updating ${theme.name}\n`));
382
- return false;
383
- }
384
- const newDetails = await this.getThemeDetails(theme.name);
385
- if (!newDetails || newDetails.version === details.version) {
386
- stderr.write(ansis.red(' Update failed!\n'));
387
- return false;
388
- }
389
- // Ensure branch is created before making our first commit
390
- await this.ensureBranchCreated(wpConfig, branchRef);
391
- const commitMessage = this.sanitizeCommitMessage(`Update theme ${prettyTitle} to ${newDetails.version}`);
392
- await execC(gitCommand, ['add', location], { context });
393
- await execC(gitCommand, ['commit', '-m', commitMessage], {
394
- context,
395
- shell: false,
396
- env: { SKIP: 'prepare-commit-msg' },
397
- });
398
- stdout.write(ansis.green(` Updated theme ${prettyTitle} from ${fromVersion} to ${newDetails.version}\n`));
399
- return true;
428
+ const result = await execC(gitCommand, ['config', '--get', 'core.hooksPath'], { stderr, reject: false });
429
+ const originalHookPath = String(result.stdout || '').trim();
430
+ await execC(gitCommand, ['config', 'core.hooksPath', '/dev/null'], { context });
431
+ return originalHookPath;
400
432
  }
401
- catch (error) {
402
- stderr.write(ansis.red(` Error updating ${theme.name}: ${error}\n`));
403
- return false;
433
+ catch (_a) {
434
+ await execC(gitCommand, ['config', 'core.hooksPath', '/dev/null'], { context });
435
+ return '';
404
436
  }
405
437
  }
406
- async getPluginDetails(pluginName) {
407
- return await this.getDetails(pluginName, 'plugin');
408
- }
409
- async getThemeDetails(themeName) {
410
- return await this.getDetails(themeName, 'theme');
438
+ async rollbackGitHooks(originalHookPath) {
439
+ const { config, context } = this;
440
+ const gitCommand = await config.command('git');
441
+ if (originalHookPath) {
442
+ await execC(gitCommand, ['config', 'core.hooksPath', originalHookPath], { context });
443
+ }
444
+ else {
445
+ await execC(gitCommand, ['config', '--unset', 'core.hooksPath'], { context, reject: false });
446
+ }
411
447
  }
412
448
  async getDetails(name, type) {
413
- const { config } = this;
414
- const cmdType = type === 'plugin' ? 'plugin' : 'theme';
415
449
  try {
416
- const yarnCommand = await config.command('yarn');
417
- const detailsResult = await execC(yarnCommand, ['jolt', 'wp', 'cli', cmdType, 'get', '--json', name], {
418
- reject: false,
419
- });
420
- const out = detailsResult.exitCode === 0 ? String(detailsResult.stdout || '') : null;
421
- if (!out) {
450
+ const result = await this.executeWpCli([type, 'get', '--json', name], { silent: true });
451
+ if (!result.stdout) {
422
452
  return null;
423
453
  }
424
- let output = String(out || '');
454
+ let output = result.stdout;
425
455
  if (output.startsWith('$')) {
426
456
  // Older Yarn versions include the script name in stdout so we need to trim the first line off
427
457
  output = output.substring(1 + output.indexOf('\n'));
@@ -449,8 +479,10 @@ export class WPUpdateCommand extends JoltCommand {
449
479
  const newVersion = await this.hasCoreUpdate();
450
480
  if (!newVersion) {
451
481
  stdout.write(ansis.dim(' WordPress core is up to date\n'));
452
- return false;
482
+ return { updated: false };
453
483
  }
484
+ // Get current version before updating
485
+ const currentVersion = await this.getCurrentCoreVersion();
454
486
  stdout.write(ansis.green(` Updating WordPress core to ${newVersion}\n`));
455
487
  const shouldStash = await this.hasGitChanges(wpConfig.wpRoot);
456
488
  if (shouldStash) {
@@ -460,7 +492,13 @@ export class WPUpdateCommand extends JoltCommand {
460
492
  try {
461
493
  await this.doCoreUpdate(wpConfig.wpRoot, newVersion, wpConfig, branchRef);
462
494
  stdout.write(ansis.green(` Updated WordPress core to ${newVersion}\n`));
463
- return true;
495
+ return {
496
+ updated: true,
497
+ details: {
498
+ fromVersion: currentVersion || 'unknown',
499
+ toVersion: newVersion,
500
+ },
501
+ };
464
502
  }
465
503
  finally {
466
504
  if (shouldStash) {
@@ -469,22 +507,23 @@ export class WPUpdateCommand extends JoltCommand {
469
507
  }
470
508
  }
471
509
  }
510
+ async getCurrentCoreVersion() {
511
+ try {
512
+ const result = await this.executeWpCli(['core', 'version'], { silent: true });
513
+ return result.stdout ? result.stdout.trim() : null;
514
+ }
515
+ catch (_a) {
516
+ return null;
517
+ }
518
+ }
472
519
  async hasCoreUpdate() {
473
520
  var _a;
474
- const { config } = this;
475
521
  try {
476
- const yarnCommand = await config.command('yarn');
477
- const coreResult = await execC(yarnCommand, ['jolt', 'wp', 'cli', 'core', 'check-update', '--json'], {
478
- reject: false,
479
- });
480
- if (!coreResult || coreResult.exitCode !== 0) {
481
- return false;
482
- }
483
- const stdoutStr = String(coreResult.stdout || '');
484
- if (!stdoutStr.trim() || stdoutStr.trim() === '[]') {
522
+ const result = await this.executeWpCli(['core', 'check-update', '--json'], { silent: true });
523
+ if (!result.stdout || !result.stdout.trim() || result.stdout.trim() === '[]') {
485
524
  return false;
486
525
  }
487
- const trimmed = stdoutStr.substring(stdoutStr.indexOf('[{'));
526
+ const trimmed = result.stdout.substring(result.stdout.indexOf('[{'));
488
527
  const parsed = JSON.parse(trimmed);
489
528
  return ((_a = parsed[0]) === null || _a === void 0 ? void 0 : _a.version) || false;
490
529
  }
@@ -493,11 +532,10 @@ export class WPUpdateCommand extends JoltCommand {
493
532
  }
494
533
  }
495
534
  async hasGitChanges(path) {
496
- const { config, context } = this;
535
+ const { config } = this;
497
536
  try {
498
537
  const gitCommand = await config.command('git');
499
538
  const result = await execC(gitCommand, ['status', '--porcelain=v1', '--', path], {
500
- context,
501
539
  reject: false,
502
540
  });
503
541
  return String(result.stdout || '').trim() !== '';
@@ -507,36 +545,124 @@ export class WPUpdateCommand extends JoltCommand {
507
545
  }
508
546
  }
509
547
  async stashChanges() {
510
- const { config, context } = this;
548
+ const { config } = this;
511
549
  const date = new Date().toISOString();
512
550
  const gitCommand = await config.command('git');
513
- await execC(gitCommand, ['add', '.'], { context });
551
+ await execC(gitCommand, ['add', '.']);
514
552
  await execC(gitCommand, ['stash', 'save', '--', `Automated stash by Jolt WP Updater at ${date}`], {
515
- context,
516
553
  shell: false,
517
554
  });
518
555
  }
519
556
  async unstashChanges() {
520
- const { config, context } = this;
557
+ const { config } = this;
521
558
  const gitCommand = await config.command('git');
522
- await execC(gitCommand, ['stash', 'pop'], { context });
523
- await execC(gitCommand, ['reset', 'HEAD', '--'], { context });
559
+ await execC(gitCommand, ['stash', 'pop']);
560
+ await execC(gitCommand, ['reset', 'HEAD', '--']);
524
561
  }
525
562
  async doCoreUpdate(path, version, wpConfig, branchRef) {
526
- const { config, context } = this;
563
+ const { config } = this;
527
564
  // Ensure branch is created before making our first commit
528
565
  await this.ensureBranchCreated(wpConfig, branchRef);
529
566
  const gitCommand = await config.command('git');
530
- const yarnCommand = await config.command('yarn');
531
- await execC(yarnCommand, ['jolt', 'wp', 'cli', 'core', 'update'], { context, reject: false });
532
- await execC(gitCommand, ['add', path], { context });
567
+ await this.executeWpCli(['core', 'update'], { silent: true });
568
+ await execC(gitCommand, ['add', path]);
533
569
  const commitMessage = this.sanitizeCommitMessage(`Update WordPress to ${version}`);
534
570
  await execC(gitCommand, ['commit', '-m', commitMessage], {
535
- context,
536
571
  shell: false,
537
572
  env: { SKIP: 'prepare-commit-msg' },
538
573
  });
539
574
  }
575
+ async maybeUpdateTranslations(branchRef) {
576
+ const { config, context: { stdout, stderr }, } = this;
577
+ try {
578
+ let hasUpdates = false;
579
+ // Helper to check and update translations for a specific type
580
+ const updateTranslationType = async (type, command) => {
581
+ stdout.write(` Checking ${type} translations...`);
582
+ const result = await this.executeWpCli(command, { silent: true });
583
+ if (result.exitCode === 0 && result.stdout) {
584
+ if (result.stdout.includes('Updated') || result.stdout.includes('updated')) {
585
+ hasUpdates = true;
586
+ stdout.write(ansis.green(' updated\n'));
587
+ }
588
+ else {
589
+ stdout.write(ansis.dim(' up to date\n'));
590
+ }
591
+ }
592
+ else {
593
+ stdout.write(ansis.dim(' skipped (not available)\n'));
594
+ }
595
+ };
596
+ // Update different translation types
597
+ await updateTranslationType('core', ['language', 'core', 'update']);
598
+ await updateTranslationType('plugin', ['language', 'plugin', 'update', '--all']);
599
+ await updateTranslationType('theme', ['language', 'theme', 'update', '--all']);
600
+ // If we have translation updates, commit them
601
+ if (hasUpdates) {
602
+ const wpConfig = await config.loadWordPressConfig();
603
+ if (!wpConfig) {
604
+ stderr.write(ansis.red('Failed to load WordPress configuration\n'));
605
+ return false;
606
+ }
607
+ // Create or ensure we're on the update branch
608
+ await this.ensureBranchCreated(wpConfig, branchRef);
609
+ const gitCommand = await config.command('git');
610
+ // Add all language files that might have been updated
611
+ await execC(gitCommand, ['add', wpConfig.wpRoot]);
612
+ // Check if there are any changes to commit
613
+ const statusResult = await execC(gitCommand, ['diff', '--cached', '--exit-code'], {
614
+ reject: false,
615
+ });
616
+ if (statusResult.exitCode !== 0) {
617
+ // There are changes to commit
618
+ await execC(gitCommand, ['commit', '-m', 'Update translations'], {
619
+ shell: false,
620
+ env: { SKIP: 'prepare-commit-msg' },
621
+ });
622
+ stdout.write(ansis.green(' Committed translation updates\n'));
623
+ return true;
624
+ }
625
+ stdout.write(ansis.dim(' No translation files changed\n'));
626
+ }
627
+ return false;
628
+ }
629
+ catch (error) {
630
+ stderr.write(ansis.red(`Error updating translations: ${error}\n`));
631
+ return false;
632
+ }
633
+ }
634
+ /**
635
+ * Update Redis drop-in from plugin files if WP-CLI fails
636
+ */
637
+ async updateRedisDropinFromPlugin(item, wpConfig, itemConfig) {
638
+ const pluginBase = path.join(itemConfig.getFolder(wpConfig), item.name);
639
+ const candidatePaths = [
640
+ path.join(pluginBase, 'object-cache.php'),
641
+ path.join(pluginBase, 'includes', 'object-cache.php'),
642
+ path.join(pluginBase, 'drop-in', 'object-cache.php'),
643
+ ];
644
+ const wpContentDir = path.join(wpConfig.wpRoot, 'wp-content');
645
+ const dropinDestLocal = path.join(wpContentDir, 'object-cache.php');
646
+ const { config } = this;
647
+ for (const candidate of candidatePaths) {
648
+ if (!(await fileExists(candidate))) {
649
+ continue;
650
+ }
651
+ try {
652
+ const contents = String(await fs.readFile(candidate));
653
+ if (!/Redis Object Cache|Redis\b/i.test(contents)) {
654
+ break;
655
+ }
656
+ await fs.copyFile(candidate, dropinDestLocal);
657
+ const gitCommand = await config.command('git');
658
+ await execC(gitCommand, ['add', dropinDestLocal]);
659
+ break;
660
+ }
661
+ catch (_a) {
662
+ break;
663
+ }
664
+ }
665
+ }
540
666
  }
541
667
  WPUpdateCommand.paths = [['wp', 'update']];
542
668
  export class WPUpdateMergeCommand extends JoltCommand {
@@ -720,6 +846,9 @@ export class WPUpdateModifyCommand extends JoltCommand {
720
846
  constructor() {
721
847
  super(...arguments);
722
848
  this.requiredCommands = ['git'];
849
+ this.autostash = Option.Boolean('--autostash', false, {
850
+ description: 'Automatically stash and unstash changes before and after the rebase',
851
+ });
723
852
  }
724
853
  async command() {
725
854
  const { config, context, context: { stdout, stderr }, logo, } = this;
@@ -732,7 +861,7 @@ export class WPUpdateModifyCommand extends JoltCommand {
732
861
  try {
733
862
  const gitCommand = await config.command('git');
734
863
  // Get current branch
735
- const currentBranchResult = await execC(gitCommand, ['branch', '--show-current'], { context });
864
+ const currentBranchResult = await execC(gitCommand, ['branch', '--show-current']);
736
865
  const currentBranch = String(currentBranchResult.stdout || '').trim();
737
866
  if (!currentBranch.startsWith('joltWpUpdate/')) {
738
867
  stderr.write(ansis.red('Not currently on a WordPress update branch\n'));
@@ -741,7 +870,6 @@ export class WPUpdateModifyCommand extends JoltCommand {
741
870
  // Count commits since branching from main
742
871
  const branchName = (await config.get('branch')) || 'master';
743
872
  const commitCountResult = await execC(gitCommand, ['rev-list', '--count', `${branchName}..HEAD`], {
744
- context,
745
873
  reject: false,
746
874
  });
747
875
  if (commitCountResult.exitCode !== 0) {
@@ -755,7 +883,11 @@ export class WPUpdateModifyCommand extends JoltCommand {
755
883
  }
756
884
  stdout.write(ansis.cyan(`📋 Starting interactive rebase for ${commitCount} commits...\n`));
757
885
  // Run interactive rebase
758
- const rebaseResult = await execC(gitCommand, ['rebase', '-i', `HEAD~${commitCount}`], {
886
+ const rebaseArgs = ['rebase', '-i', `HEAD~${commitCount}`];
887
+ if (this.autostash) {
888
+ rebaseArgs.push('--autostash');
889
+ }
890
+ const rebaseResult = await execC(gitCommand, rebaseArgs, {
759
891
  context,
760
892
  reject: false,
761
893
  });