@loopress/cli 0.24.1 → 0.26.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/README.md +721 -90
- package/dist/commands/acf/push.d.ts +1 -0
- package/dist/commands/acf/push.js +25 -2
- package/dist/commands/acf/rollback.d.ts +2 -0
- package/dist/commands/acf/rollback.js +5 -0
- package/dist/commands/api/rollback.d.ts +2 -0
- package/dist/commands/api/rollback.js +5 -0
- package/dist/commands/diff.js +1 -1
- package/dist/commands/doctor.d.ts +17 -1
- package/dist/commands/doctor.js +55 -38
- package/dist/commands/form/pull.js +4 -1
- package/dist/commands/form/push.d.ts +1 -0
- package/dist/commands/form/push.js +29 -3
- package/dist/commands/form/rollback.d.ts +2 -0
- package/dist/commands/form/rollback.js +5 -0
- package/dist/commands/hook/rollback.d.ts +2 -0
- package/dist/commands/hook/rollback.js +5 -0
- package/dist/commands/init.js +2 -0
- package/dist/commands/menu/diff.d.ts +2 -0
- package/dist/commands/menu/diff.js +5 -0
- package/dist/commands/menu/list.d.ts +13 -0
- package/dist/commands/menu/list.js +32 -0
- package/dist/commands/menu/pull.d.ts +15 -0
- package/dist/commands/menu/pull.js +72 -0
- package/dist/commands/menu/push.d.ts +17 -0
- package/dist/commands/menu/push.js +120 -0
- package/dist/commands/menu/rollback.d.ts +2 -0
- package/dist/commands/menu/rollback.js +5 -0
- package/dist/commands/option/add.js +1 -1
- package/dist/commands/option/push.d.ts +1 -0
- package/dist/commands/option/push.js +26 -1
- package/dist/commands/option/rollback.d.ts +2 -0
- package/dist/commands/option/rollback.js +5 -0
- package/dist/commands/page/diff.d.ts +2 -0
- package/dist/commands/page/diff.js +5 -0
- package/dist/commands/page/list.d.ts +8 -0
- package/dist/commands/page/list.js +22 -0
- package/dist/commands/page/push.d.ts +20 -0
- package/dist/commands/page/push.js +60 -0
- package/dist/commands/plugin/push.d.ts +1 -0
- package/dist/commands/plugin/push.js +14 -0
- package/dist/commands/promote.d.ts +8 -1
- package/dist/commands/promote.js +2 -0
- package/dist/commands/pull.d.ts +11 -1
- package/dist/commands/pull.js +9 -2
- package/dist/commands/push.d.ts +11 -1
- package/dist/commands/push.js +10 -2
- package/dist/commands/seo/pull.js +6 -2
- package/dist/commands/seo/push.d.ts +1 -0
- package/dist/commands/seo/push.js +33 -3
- package/dist/commands/seo/rollback.d.ts +2 -0
- package/dist/commands/seo/rollback.js +5 -0
- package/dist/commands/snippet/push.d.ts +1 -0
- package/dist/commands/snippet/push.js +31 -1
- package/dist/commands/snippet/rollback.d.ts +2 -0
- package/dist/commands/snippet/rollback.js +5 -0
- package/dist/commands/theme-styles/diff.d.ts +2 -0
- package/dist/commands/theme-styles/diff.js +5 -0
- package/dist/commands/theme-styles/pull.d.ts +12 -0
- package/dist/commands/theme-styles/pull.js +31 -0
- package/dist/commands/theme-styles/push.d.ts +12 -0
- package/dist/commands/theme-styles/push.js +45 -0
- package/dist/commands/theme-styles/rollback.d.ts +2 -0
- package/dist/commands/theme-styles/rollback.js +5 -0
- package/dist/lib/app-manifest.js +4 -27
- package/dist/lib/base.d.ts +2 -0
- package/dist/lib/base.js +6 -0
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +2 -1
- package/dist/lib/find-orphaned-files.js +5 -47
- package/dist/lib/load-files.js +4 -42
- package/dist/lib/php-files-command.d.ts +1 -0
- package/dist/lib/php-files-command.js +84 -47
- package/dist/lib/push-command.d.ts +4 -0
- package/dist/lib/push-command.js +49 -0
- package/dist/lib/readdir-tolerant.d.ts +1 -0
- package/dist/lib/readdir-tolerant.js +12 -0
- package/dist/lib/resource-state.js +116 -3
- package/dist/lib/rollback-command.d.ts +21 -0
- package/dist/lib/rollback-command.js +180 -0
- package/dist/lib/rollback-materialize.d.ts +1 -0
- package/dist/lib/rollback-materialize.js +121 -0
- package/dist/lib/snapshot-store.d.ts +30 -0
- package/dist/lib/snapshot-store.js +133 -0
- package/dist/lib/validate-local.js +7 -1
- package/dist/types/project-config.generated.d.ts +12 -0
- package/dist/utils/acf-format.d.ts +1 -0
- package/dist/utils/acf-format.js +6 -0
- package/dist/utils/form-format.d.ts +4 -0
- package/dist/utils/form-format.js +3 -7
- package/dist/utils/menu-format.d.ts +24 -0
- package/dist/utils/menu-format.js +8 -0
- package/dist/utils/option-format.d.ts +1 -0
- package/dist/utils/page-format.d.ts +27 -0
- package/dist/utils/page-format.js +111 -0
- package/dist/utils/resource-dirs.d.ts +3 -0
- package/dist/utils/resource-dirs.js +6 -0
- package/dist/utils/seo-format.d.ts +8 -0
- package/dist/utils/seo-format.js +3 -0
- package/dist/utils/snippet-format.d.ts +1 -0
- package/dist/utils/snippet-format.js +1 -0
- package/dist/utils/theme-styles-format.d.ts +26 -0
- package/dist/utils/theme-styles-format.js +55 -0
- package/oclif.manifest.json +1633 -168
- package/package.json +4 -2
- package/schemas/project-config.schema.json +15 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { extname, join } from 'node:path';
|
|
4
|
+
import { PushCommand } from '../../lib/push-command.js';
|
|
5
|
+
import { readdirTolerant } from '../../lib/readdir-tolerant.js';
|
|
6
|
+
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
7
|
+
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
8
|
+
import { getMenuSlug, MENU_ENDPOINT, MENU_LOCATIONS_ENDPOINT, menuEndpoint } from '../../utils/menu-format.js';
|
|
9
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
10
|
+
// A menu literally slugged "menu-locations" would still collide, but that's far less likely
|
|
11
|
+
// than the plain "locations" this used to be (WordPress's own menu-locations feature).
|
|
12
|
+
const LOCATIONS_FILENAME = 'menu-locations.json';
|
|
13
|
+
export default class Push extends PushCommand {
|
|
14
|
+
static args = {
|
|
15
|
+
path: Args.string({ description: 'Path to menus directory (overrides project config)' }),
|
|
16
|
+
};
|
|
17
|
+
static description = 'Push local nav menus and the active theme menu locations to WordPress. Each menu\'s post_type/taxonomy items are resolved by slug on the target environment, never by a raw id; an item whose target does not exist there fails the whole menu rather than guessing.';
|
|
18
|
+
static examples = ['$ lps menu push'];
|
|
19
|
+
static flags = {
|
|
20
|
+
...PushCommand.dryRunFlag,
|
|
21
|
+
...PushCommand.yesFlag,
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { args } = await this.parse(Push);
|
|
25
|
+
const { url } = this.siteConfig;
|
|
26
|
+
const path = this.resolveMenuPath(args.path);
|
|
27
|
+
this.log(`Pushing nav menus to ${url}`);
|
|
28
|
+
this.log(`Menus path: ${path}`);
|
|
29
|
+
const provider = getResourceStateProvider('menu');
|
|
30
|
+
const beforeState = await this.captureBeforePushState(provider, path);
|
|
31
|
+
await this.pushMenus(path);
|
|
32
|
+
await this.pushLocations(path);
|
|
33
|
+
await this.writeAfterPushSnapshot(provider, path, beforeState);
|
|
34
|
+
if (this.failedCount > 0) {
|
|
35
|
+
this.error(`${pluralize(this.failedCount, 'menu')} failed to push.`);
|
|
36
|
+
}
|
|
37
|
+
if (this.dryRun)
|
|
38
|
+
return;
|
|
39
|
+
await this.recordSuccess();
|
|
40
|
+
this.log('All nav menus pushed.');
|
|
41
|
+
}
|
|
42
|
+
async currentRevision(slug) {
|
|
43
|
+
try {
|
|
44
|
+
const current = await this.wp.get(menuEndpoint(slug));
|
|
45
|
+
return current.revision;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (isNotFoundError(error))
|
|
49
|
+
return undefined;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async pushLocations(basePath) {
|
|
54
|
+
const file = join(basePath, LOCATIONS_FILENAME);
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
raw = await readFile(file, 'utf8');
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code === 'ENOENT')
|
|
61
|
+
return;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
if (this.dryRun) {
|
|
65
|
+
this.log(`[dry-run] Would push: ${file}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
await this.wp.put(MENU_LOCATIONS_ENDPOINT, JSON.parse(raw));
|
|
70
|
+
this.log(`Pushed: ${file}`);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.failedCount++;
|
|
74
|
+
this.warn(`Failed to push ${file}: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async pushMenuFile(filePath, task) {
|
|
78
|
+
if (this.dryRun) {
|
|
79
|
+
if (task)
|
|
80
|
+
task.output = `[dry-run] Would push: ${filePath}`;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(await readFile(filePath, 'utf8'));
|
|
85
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
86
|
+
throw new Error('not a JSON object');
|
|
87
|
+
const record = parsed;
|
|
88
|
+
const slug = getMenuSlug(record);
|
|
89
|
+
if (slug === null)
|
|
90
|
+
throw new Error('missing or invalid "slug"');
|
|
91
|
+
const name = typeof record.name === 'string' ? record.name : '';
|
|
92
|
+
const items = record.items ?? [];
|
|
93
|
+
if (!Array.isArray(items))
|
|
94
|
+
throw new Error('"items" must be an array');
|
|
95
|
+
// Read the menu's current revision right before writing it, and send it back as
|
|
96
|
+
// `expectedRevision`: WordPress refuses the write (412) if something else changed the menu
|
|
97
|
+
// in between, instead of this push silently overwriting it (#234). No revision to condition
|
|
98
|
+
// on for a menu that doesn't exist remotely yet (a first push, an upsert create): falls back
|
|
99
|
+
// to today's unconditional write, same as before this existed.
|
|
100
|
+
const expectedRevision = await this.currentRevision(slug);
|
|
101
|
+
const body = { items, name, slug };
|
|
102
|
+
if (expectedRevision !== undefined)
|
|
103
|
+
body.expectedRevision = expectedRevision;
|
|
104
|
+
const result = await this.wp.post(MENU_ENDPOINT, body);
|
|
105
|
+
if (task) {
|
|
106
|
+
task.output = result.warnings.length > 0 ? `Pushed: ${slug} (warning: ${result.warnings.join('; ')})` : `Pushed: ${slug}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
this.reportTaskFailure(`Failed to push ${filePath}: ${error.message}`, error, task);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async pushMenus(basePath) {
|
|
114
|
+
const files = (await readdirTolerant(basePath)).filter((file) => extname(file) === '.json' && file !== LOCATIONS_FILENAME);
|
|
115
|
+
if (files.length === 0)
|
|
116
|
+
return;
|
|
117
|
+
this.log(`Found ${pluralize(files.length, 'menu')} to push`);
|
|
118
|
+
await this.runPushTasks(files, (file) => file, async (file, task) => this.pushMenuFile(join(basePath, file), task));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceRollbackCommand } from '../../lib/rollback-command.js';
|
|
2
|
+
export default resourceRollbackCommand('menu', {
|
|
3
|
+
description: 'Restore nav menus and menu locations to a snapshot taken automatically before an earlier `lps menu push`',
|
|
4
|
+
pathNoun: 'menus directory',
|
|
5
|
+
});
|
|
@@ -33,7 +33,7 @@ export default class Add extends LoopressCommand {
|
|
|
33
33
|
}
|
|
34
34
|
throw error;
|
|
35
35
|
}
|
|
36
|
-
const local = {
|
|
36
|
+
const local = { autoload: remote.autoload, name: remote.name, readonly: defaultReadonlyFor(name), value: remote.value };
|
|
37
37
|
const file = join(dir, optionFileName(name));
|
|
38
38
|
if (this.dryRun) {
|
|
39
39
|
this.log(`[dry-run] Would track "${name}" at ${file}`);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { loadFiles } from '../../lib/load-files.js';
|
|
3
3
|
import { PushCommand } from '../../lib/push-command.js';
|
|
4
|
+
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
5
|
+
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
4
6
|
import { optionEndpoint, parseLocalOption, partitionByReadonly } from '../../utils/option-format.js';
|
|
5
7
|
import { pluralize } from '../../utils/pluralize.js';
|
|
6
8
|
export default class Push extends PushCommand {
|
|
@@ -21,6 +23,8 @@ export default class Push extends PushCommand {
|
|
|
21
23
|
const { url } = this.siteConfig;
|
|
22
24
|
this.log(`Pushing tracked options to ${url}`);
|
|
23
25
|
this.log(`Options path: ${dir}`);
|
|
26
|
+
const provider = getResourceStateProvider('option');
|
|
27
|
+
const beforeState = await this.captureBeforePushState(provider, dir);
|
|
24
28
|
const tracked = await loadFiles(dir, {
|
|
25
29
|
extension: '.json',
|
|
26
30
|
onSkip: (message) => {
|
|
@@ -38,6 +42,7 @@ export default class Push extends PushCommand {
|
|
|
38
42
|
await this.pushOption(option, task);
|
|
39
43
|
pushed.push(option.name);
|
|
40
44
|
});
|
|
45
|
+
await this.writeAfterPushSnapshot(provider, dir, beforeState);
|
|
41
46
|
if (this.failedCount > 0) {
|
|
42
47
|
this.error(`${pluralize(this.failedCount, 'option')} failed to push.`);
|
|
43
48
|
}
|
|
@@ -48,6 +53,17 @@ export default class Push extends PushCommand {
|
|
|
48
53
|
this.log('All options pushed.');
|
|
49
54
|
return { pushed, skipped: skippedNames, status: 'success' };
|
|
50
55
|
}
|
|
56
|
+
async currentRevision(name) {
|
|
57
|
+
try {
|
|
58
|
+
const current = await this.wp.get(optionEndpoint(name));
|
|
59
|
+
return current.revision;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (isNotFoundError(error))
|
|
63
|
+
return undefined;
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
51
67
|
async pushOption(option, task) {
|
|
52
68
|
if (this.dryRun) {
|
|
53
69
|
if (task)
|
|
@@ -55,7 +71,16 @@ export default class Push extends PushCommand {
|
|
|
55
71
|
return;
|
|
56
72
|
}
|
|
57
73
|
try {
|
|
58
|
-
|
|
74
|
+
// Read the option's current revision right before writing it, and send it back as
|
|
75
|
+
// `expectedRevision`: WordPress refuses the write (412) if something else changed the
|
|
76
|
+
// option in between, instead of this push silently overwriting it (#234). No revision to
|
|
77
|
+
// condition on for an option that doesn't exist remotely yet (a first push, an upsert
|
|
78
|
+
// create): falls back to today's unconditional write, same as before this existed.
|
|
79
|
+
const expectedRevision = await this.currentRevision(option.name);
|
|
80
|
+
const body = { autoload: option.autoload, value: option.value };
|
|
81
|
+
if (expectedRevision !== undefined)
|
|
82
|
+
body.expectedRevision = expectedRevision;
|
|
83
|
+
await this.wp.put(optionEndpoint(option.name), body);
|
|
59
84
|
if (task)
|
|
60
85
|
task.output = `Pushed: ${option.name}`;
|
|
61
86
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceRollbackCommand } from '../../lib/rollback-command.js';
|
|
2
|
+
export default resourceRollbackCommand('option', {
|
|
3
|
+
description: 'Restore tracked options to a snapshot taken automatically before an earlier `lps option push`',
|
|
4
|
+
pathNoun: 'options directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('page', {
|
|
3
|
+
description: 'Show what differs in static pages between your local files and a WordPress environment, or between two environments',
|
|
4
|
+
pathNoun: 'pages directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { type RemotePage } from '../../utils/page-format.js';
|
|
3
|
+
export default class List extends LoopressCommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static enableJsonFlag: boolean;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
run(): Promise<Array<Omit<RemotePage, 'html'>>>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { PAGES_ENDPOINT } from '../../utils/page-format.js';
|
|
3
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
4
|
+
export default class List extends LoopressCommand {
|
|
5
|
+
static description = 'List the static pages managed by Loopress on WordPress';
|
|
6
|
+
static enableJsonFlag = true;
|
|
7
|
+
static examples = ['$ lps page list'];
|
|
8
|
+
async run() {
|
|
9
|
+
const pages = await this.wp.get(PAGES_ENDPOINT);
|
|
10
|
+
// The HTML is only needed by `page diff`, it would drown the JSON output here.
|
|
11
|
+
const listed = pages.map(({ html: _html, ...page }) => page);
|
|
12
|
+
if (listed.length === 0) {
|
|
13
|
+
this.log('No pages managed by Loopress');
|
|
14
|
+
return listed;
|
|
15
|
+
}
|
|
16
|
+
this.log(`Found ${pluralize(listed.length, 'page')}:`);
|
|
17
|
+
this.log('');
|
|
18
|
+
for (const page of listed)
|
|
19
|
+
this.log(` ${page.slug} ${page.status} ${page.link}`);
|
|
20
|
+
return listed;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PushCommand } from '../../lib/push-command.js';
|
|
2
|
+
type PushResult = {
|
|
3
|
+
pushed: string[];
|
|
4
|
+
status: 'dry-run' | 'success';
|
|
5
|
+
};
|
|
6
|
+
export default class Push extends PushCommand {
|
|
7
|
+
static args: {
|
|
8
|
+
slug: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
9
|
+
};
|
|
10
|
+
static description: string;
|
|
11
|
+
static enableJsonFlag: boolean;
|
|
12
|
+
static examples: string[];
|
|
13
|
+
static flags: {
|
|
14
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<PushResult>;
|
|
18
|
+
private pushPage;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { PushCommand } from '../../lib/push-command.js';
|
|
3
|
+
import { formatPageProblems, PAGES_ENDPOINT, readLocalPages } from '../../utils/page-format.js';
|
|
4
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
5
|
+
import { resolveResourceDir } from '../../utils/resource-dirs.js';
|
|
6
|
+
export default class Push extends PushCommand {
|
|
7
|
+
static args = {
|
|
8
|
+
slug: Args.string({ description: 'Push only this page (its file name without .html). Pushes every page when omitted.' }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Push static HTML pages (pages/<slug>.html) to WordPress. The `status` header (draft or publish, draft by default) is applied on every push.';
|
|
11
|
+
static enableJsonFlag = true;
|
|
12
|
+
static examples = ['$ lps page push', '$ lps page push legal-notice', '$ lps page push --dry-run'];
|
|
13
|
+
static flags = {
|
|
14
|
+
...PushCommand.dryRunFlag,
|
|
15
|
+
...PushCommand.yesFlag,
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { args } = await this.parse(Push);
|
|
19
|
+
const path = resolveResourceDir('page', this.localConfig);
|
|
20
|
+
this.log(`Pushing pages to ${this.siteConfig.url}`);
|
|
21
|
+
this.log(`Pages path: ${path}`);
|
|
22
|
+
const { pages, problems } = await readLocalPages(path);
|
|
23
|
+
if (problems.length > 0) {
|
|
24
|
+
this.error(`Nothing was pushed, fix these pages first:\n ${formatPageProblems(problems)}`);
|
|
25
|
+
}
|
|
26
|
+
const selected = args.slug === undefined ? pages : pages.filter((page) => page.slug === args.slug);
|
|
27
|
+
if (args.slug !== undefined && selected.length === 0) {
|
|
28
|
+
this.error(`No page "${args.slug}" in ${path} (expected ${args.slug}.html).`);
|
|
29
|
+
}
|
|
30
|
+
this.log(`Found ${pluralize(selected.length, 'page')} to push`);
|
|
31
|
+
const pushed = [];
|
|
32
|
+
await this.runPushTasks(selected, (page) => page.slug, async (page, task) => {
|
|
33
|
+
await this.pushPage(page, task);
|
|
34
|
+
pushed.push(page.slug);
|
|
35
|
+
});
|
|
36
|
+
if (this.failedCount > 0) {
|
|
37
|
+
this.error(`${pluralize(this.failedCount, 'page')} failed to push.`);
|
|
38
|
+
}
|
|
39
|
+
if (this.dryRun)
|
|
40
|
+
return { pushed, status: 'dry-run' };
|
|
41
|
+
await this.recordSuccess();
|
|
42
|
+
this.log('All pages pushed.');
|
|
43
|
+
return { pushed, status: 'success' };
|
|
44
|
+
}
|
|
45
|
+
async pushPage(page, task) {
|
|
46
|
+
if (this.dryRun) {
|
|
47
|
+
if (task)
|
|
48
|
+
task.output = `[dry-run] Would push: ${page.slug} (${page.status})`;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const result = await this.wp.put(PAGES_ENDPOINT, page);
|
|
53
|
+
if (task)
|
|
54
|
+
task.output = `Pushed: ${page.slug} (${result.status}) ${result.link}`;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.reportTaskFailure(`Failed to push ${page.slug}: ${error.message}`, error, task);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -70,6 +70,7 @@ export default class Push extends PushCommand {
|
|
|
70
70
|
await this.activate(deactivated.filter((p) => !diff.toRemove.includes(p.slug)));
|
|
71
71
|
throw error;
|
|
72
72
|
}
|
|
73
|
+
toReactivate.push(...(await this.filesForNewlyInstalled(diff.toInstall)));
|
|
73
74
|
if (toPrune.length > 0) {
|
|
74
75
|
await this.deactivate(installed.filter((p) => toPrune.includes(p.slug)));
|
|
75
76
|
}
|
|
@@ -126,6 +127,19 @@ export default class Push extends PushCommand {
|
|
|
126
127
|
throw error;
|
|
127
128
|
}
|
|
128
129
|
}
|
|
130
|
+
// toInstall entries carry no `file`: the plugin didn't exist on the site at diff time, so its
|
|
131
|
+
// WordPress core plugin id (folder/main-file.php) is only knowable after sync() has actually
|
|
132
|
+
// installed it. Refetch to get it, then activate like everything else.
|
|
133
|
+
async filesForNewlyInstalled(toInstall) {
|
|
134
|
+
if (toInstall.length === 0)
|
|
135
|
+
return [];
|
|
136
|
+
const installedAfterSync = parseInstalledPlugins(await this.wp.get('wp/v2/plugins'));
|
|
137
|
+
const fileBySlug = new Map(installedAfterSync.map((p) => [p.slug, p.file]));
|
|
138
|
+
return toInstall.flatMap(({ slug }) => {
|
|
139
|
+
const file = fileBySlug.get(slug);
|
|
140
|
+
return file ? [{ file, slug }] : [];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
129
143
|
guardForce(diff, force) {
|
|
130
144
|
if (force)
|
|
131
145
|
return;
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
type PromoteResult = {
|
|
3
|
+
from: string;
|
|
4
|
+
status: 'dry-run' | 'promoted';
|
|
5
|
+
to: string;
|
|
6
|
+
};
|
|
2
7
|
export default class Promote extends Command {
|
|
3
8
|
static args: {
|
|
4
9
|
from: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
10
|
to: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
11
|
};
|
|
7
12
|
static description: string;
|
|
13
|
+
static enableJsonFlag: boolean;
|
|
8
14
|
static examples: string[];
|
|
9
15
|
static flags: {
|
|
10
16
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
17
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
18
|
};
|
|
13
|
-
run(): Promise<
|
|
19
|
+
run(): Promise<PromoteResult>;
|
|
14
20
|
private confirmPromotion;
|
|
15
21
|
private delegateArgv;
|
|
16
22
|
private requireEnvironment;
|
|
17
23
|
private resolveProject;
|
|
18
24
|
}
|
|
25
|
+
export {};
|
package/dist/commands/promote.js
CHANGED
|
@@ -10,6 +10,7 @@ export default class Promote extends Command {
|
|
|
10
10
|
to: Args.string({ description: 'Environment to copy the configuration to', required: true }),
|
|
11
11
|
};
|
|
12
12
|
static description = 'Copy every tracked resource from one environment to another by pulling from <from> then pushing to <to>. Local tracked files are overwritten with <from> in the process.';
|
|
13
|
+
static enableJsonFlag = true;
|
|
13
14
|
static examples = ['$ lps promote staging production', '$ lps promote production staging --dry-run'];
|
|
14
15
|
static flags = {
|
|
15
16
|
...LoopressCommand.dryRunFlag,
|
|
@@ -38,6 +39,7 @@ export default class Promote extends Command {
|
|
|
38
39
|
this.log(`\n=== Pushing to ${to.name} (${to.url}) ===`);
|
|
39
40
|
await this.config.runCommand('push', this.delegateArgv(to.name, dryRun));
|
|
40
41
|
this.log(dryRun ? `\n[dry-run] ${from.name} would be promoted to ${to.name}.` : `\n${from.name} promoted to ${to.name}.`);
|
|
42
|
+
return { from: from.name, status: dryRun ? 'dry-run' : 'promoted', to: to.name };
|
|
41
43
|
}
|
|
42
44
|
// Deliberately not `guardProductionPush`: promote overwrites local tracked files for every
|
|
43
45
|
// target, not just production, so it needs its own confirmation regardless of the name. That
|
package/dist/commands/pull.d.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { LoopressCommand } from '../lib/base.js';
|
|
2
|
+
type PullTargetResult = {
|
|
3
|
+
error?: string;
|
|
4
|
+
label: string;
|
|
5
|
+
status: 'failed' | 'pulled';
|
|
6
|
+
};
|
|
7
|
+
type PullResult = {
|
|
8
|
+
results: PullTargetResult[];
|
|
9
|
+
};
|
|
2
10
|
export default class Pull extends LoopressCommand {
|
|
3
11
|
static description: string;
|
|
12
|
+
static enableJsonFlag: boolean;
|
|
4
13
|
static examples: string[];
|
|
5
14
|
static flags: {
|
|
6
15
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
16
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
17
|
};
|
|
9
18
|
private failedCount;
|
|
10
|
-
run(): Promise<
|
|
19
|
+
run(): Promise<PullResult>;
|
|
11
20
|
private buildArgv;
|
|
12
21
|
}
|
|
22
|
+
export {};
|
package/dist/commands/pull.js
CHANGED
|
@@ -11,11 +11,13 @@ const PULL_TARGETS = [
|
|
|
11
11
|
{ commandId: 'hook:pull', label: 'hooks', supportsYes: true },
|
|
12
12
|
{ commandId: 'form:pull', label: 'forms', supportsYes: true },
|
|
13
13
|
{ commandId: 'seo:pull', label: 'SEO', supportsYes: true },
|
|
14
|
+
{ commandId: 'menu:pull', label: 'menus', supportsYes: true },
|
|
14
15
|
{ commandId: 'option:pull', label: 'options', supportsYes: true },
|
|
15
16
|
{ commandId: 'snippet:pull', label: 'snippets', supportsYes: true },
|
|
16
17
|
];
|
|
17
18
|
export default class Pull extends LoopressCommand {
|
|
18
|
-
static description = 'Pull all content, plugins, composer dependencies, ACF, API routes, hooks, forms, SEO, options, and snippets, from WordPress';
|
|
19
|
+
static description = 'Pull all content, plugins, composer dependencies, ACF, API routes, hooks, forms, SEO, menus, options, and snippets, from WordPress';
|
|
20
|
+
static enableJsonFlag = true;
|
|
19
21
|
static examples = ['$ lps pull', '$ lps pull --env staging', '$ lps pull --dry-run'];
|
|
20
22
|
static flags = {
|
|
21
23
|
...LoopressCommand.dryRunFlag,
|
|
@@ -23,21 +25,26 @@ export default class Pull extends LoopressCommand {
|
|
|
23
25
|
};
|
|
24
26
|
failedCount = 0;
|
|
25
27
|
async run() {
|
|
28
|
+
const results = [];
|
|
26
29
|
for (const target of PULL_TARGETS) {
|
|
27
30
|
this.log(`\n→ Pulling ${target.label}...`);
|
|
28
31
|
try {
|
|
29
32
|
await this.config.runCommand(target.commandId, this.buildArgv(target));
|
|
30
33
|
this.log(`✓ ${target.label} pulled`);
|
|
34
|
+
results.push({ label: target.label, status: 'pulled' });
|
|
31
35
|
}
|
|
32
36
|
catch (error) {
|
|
33
37
|
this.failedCount++;
|
|
34
|
-
|
|
38
|
+
const { message } = error;
|
|
39
|
+
this.log(`✗ ${target.label} failed: ${message}`);
|
|
40
|
+
results.push({ error: message, label: target.label, status: 'failed' });
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
if (this.failedCount > 0) {
|
|
38
44
|
this.error(`${pluralize(this.failedCount, 'resource')} failed to pull.`);
|
|
39
45
|
}
|
|
40
46
|
this.log('\nAll resources pulled.');
|
|
47
|
+
return { results };
|
|
41
48
|
}
|
|
42
49
|
// `--yes` is forwarded only when the user passed it and the target accepts it: unlike
|
|
43
50
|
// `lps push`, a bare `lps pull` should still prompt before removing local files.
|
package/dist/commands/push.d.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { LoopressCommand } from '../lib/base.js';
|
|
2
|
+
type PushTargetResult = {
|
|
3
|
+
error?: string;
|
|
4
|
+
label: string;
|
|
5
|
+
status: 'failed' | 'pushed';
|
|
6
|
+
};
|
|
7
|
+
type PushResult = {
|
|
8
|
+
results: PushTargetResult[];
|
|
9
|
+
};
|
|
2
10
|
export default class Push extends LoopressCommand {
|
|
3
11
|
static description: string;
|
|
12
|
+
static enableJsonFlag: boolean;
|
|
4
13
|
static examples: string[];
|
|
5
14
|
static flags: {
|
|
6
15
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
16
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
17
|
};
|
|
9
18
|
private failedCount;
|
|
10
|
-
run(): Promise<
|
|
19
|
+
run(): Promise<PushResult>;
|
|
11
20
|
private buildArgv;
|
|
12
21
|
private guardProductionPush;
|
|
13
22
|
}
|
|
23
|
+
export {};
|
package/dist/commands/push.js
CHANGED
|
@@ -10,12 +10,15 @@ const PUSH_TARGETS = [
|
|
|
10
10
|
{ commandId: 'api:push', label: 'API routes' },
|
|
11
11
|
{ commandId: 'hook:push', label: 'hooks' },
|
|
12
12
|
{ commandId: 'form:push', label: 'forms' },
|
|
13
|
+
{ commandId: 'page:push', label: 'pages' },
|
|
13
14
|
{ commandId: 'seo:push', label: 'SEO' },
|
|
15
|
+
{ commandId: 'menu:push', label: 'menus' },
|
|
14
16
|
{ commandId: 'option:push', label: 'options' },
|
|
15
17
|
{ commandId: 'snippet:push', label: 'snippets' },
|
|
16
18
|
];
|
|
17
19
|
export default class Push extends LoopressCommand {
|
|
18
|
-
static description = 'Push all local content, plugins, composer dependencies, ACF, API routes, hooks, forms, SEO, options, and snippets, to WordPress';
|
|
20
|
+
static description = 'Push all local content, plugins, composer dependencies, ACF, API routes, hooks, forms, pages, SEO, menus, options, and snippets, to WordPress';
|
|
21
|
+
static enableJsonFlag = true;
|
|
19
22
|
static examples = ['$ lps push', '$ lps push --env staging', '$ lps push --dry-run'];
|
|
20
23
|
static flags = {
|
|
21
24
|
...LoopressCommand.dryRunFlag,
|
|
@@ -25,21 +28,26 @@ export default class Push extends LoopressCommand {
|
|
|
25
28
|
async run() {
|
|
26
29
|
await this.guardProductionPush();
|
|
27
30
|
const argv = this.buildArgv();
|
|
31
|
+
const results = [];
|
|
28
32
|
for (const target of PUSH_TARGETS) {
|
|
29
33
|
this.log(`\n→ Pushing ${target.label}...`);
|
|
30
34
|
try {
|
|
31
35
|
await this.config.runCommand(target.commandId, argv);
|
|
32
36
|
this.log(`✓ ${target.label} pushed`);
|
|
37
|
+
results.push({ label: target.label, status: 'pushed' });
|
|
33
38
|
}
|
|
34
39
|
catch (error) {
|
|
35
40
|
this.failedCount++;
|
|
36
|
-
|
|
41
|
+
const { message } = error;
|
|
42
|
+
this.log(`✗ ${target.label} failed: ${message}`);
|
|
43
|
+
results.push({ error: message, label: target.label, status: 'failed' });
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
if (this.failedCount > 0) {
|
|
40
47
|
this.error(`${pluralize(this.failedCount, 'resource')} failed to push.`);
|
|
41
48
|
}
|
|
42
49
|
this.log('\nAll resources pushed.');
|
|
50
|
+
return { results };
|
|
43
51
|
}
|
|
44
52
|
// Every delegated push always gets --yes: either this command's own guard already confirmed
|
|
45
53
|
// production once above, or the target isn't production and there's nothing to confirm.
|
|
@@ -40,7 +40,9 @@ export default class Pull extends LoopressCommand {
|
|
|
40
40
|
orphanReason: `in ${dir} no longer present on WordPress`,
|
|
41
41
|
pulledMessage: `Pulled ${remote.length} ${postType} post-meta file(s) to ${dir}`,
|
|
42
42
|
title: (post) => post.slug,
|
|
43
|
-
|
|
43
|
+
// `revision` (#234) is never persisted locally, same as RemoteOption.revision: it's only
|
|
44
|
+
// ever read fresh, right before a push, never trusted from a stale local file.
|
|
45
|
+
write: async ({ meta, slug, title }, writeDir) => writeFile(join(writeDir, `${slug}.json`), JSON.stringify({ meta, slug, title }, null, 2) + '\n'),
|
|
44
46
|
});
|
|
45
47
|
}
|
|
46
48
|
// Redirects are only supported by some SeoProvider backends (RankMath, not Yoast). Unlike
|
|
@@ -71,7 +73,9 @@ export default class Pull extends LoopressCommand {
|
|
|
71
73
|
}
|
|
72
74
|
async pullSettings(basePath) {
|
|
73
75
|
const file = join(basePath, 'settings.json');
|
|
74
|
-
|
|
76
|
+
// `revision` (#234) is never persisted locally, same as RemoteOption.revision: settings.json
|
|
77
|
+
// holds only the settings themselves, exactly as it did before the wrapped response shape.
|
|
78
|
+
const { settings } = await this.wp.get(SEO_SETTINGS_ENDPOINT);
|
|
75
79
|
if (this.dryRun) {
|
|
76
80
|
this.log(`[dry-run] Would pull settings to ${file}`);
|
|
77
81
|
return;
|