@loopress/cli 0.29.0 → 0.31.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 +1369 -360
- package/dist/commands/acf/push.js +2 -1
- package/dist/commands/composer/push.d.ts +1 -0
- package/dist/commands/composer/push.js +13 -0
- package/dist/commands/content/copy.d.ts +28 -0
- package/dist/commands/content/copy.js +97 -0
- package/dist/commands/cpt/diff.js +5 -0
- package/dist/commands/cpt/list.d.ts +2 -0
- package/dist/commands/cpt/list.js +4 -0
- package/dist/commands/cpt/pull.d.ts +2 -0
- package/dist/commands/cpt/pull.js +4 -0
- package/dist/commands/cpt/push.d.ts +2 -0
- package/dist/commands/cpt/push.js +4 -0
- package/dist/commands/cpt/rm.d.ts +2 -0
- package/dist/commands/cpt/rm.js +3 -0
- package/dist/commands/cpt/rollback.js +5 -0
- package/dist/commands/dev.js +14 -4
- package/dist/commands/diff.js +1 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +57 -2
- package/dist/commands/form/push.js +4 -1
- package/dist/commands/meta/diff.d.ts +2 -0
- package/dist/commands/meta/diff.js +5 -0
- package/dist/commands/meta/list.d.ts +19 -0
- package/dist/commands/meta/list.js +21 -0
- package/dist/commands/meta/pull.d.ts +2 -0
- package/dist/commands/meta/pull.js +4 -0
- package/dist/commands/meta/push.d.ts +2 -0
- package/dist/commands/meta/push.js +4 -0
- package/dist/commands/meta/rm.d.ts +2 -0
- package/dist/commands/meta/rm.js +3 -0
- package/dist/commands/meta/rollback.d.ts +2 -0
- package/dist/commands/meta/rollback.js +5 -0
- package/dist/commands/option/list.js +2 -8
- package/dist/commands/option/push.js +4 -2
- package/dist/commands/page/push.js +3 -2
- package/dist/commands/pattern/diff.d.ts +2 -0
- package/dist/commands/pattern/diff.js +5 -0
- package/dist/commands/pattern/list.d.ts +2 -0
- package/dist/commands/pattern/list.js +3 -0
- package/dist/commands/pattern/pull.d.ts +2 -0
- package/dist/commands/pattern/pull.js +4 -0
- package/dist/commands/pattern/push.d.ts +2 -0
- package/dist/commands/pattern/push.js +4 -0
- package/dist/commands/pattern/rm.d.ts +2 -0
- package/dist/commands/pattern/rm.js +3 -0
- package/dist/commands/pattern/rollback.d.ts +2 -0
- package/dist/commands/pattern/rollback.js +5 -0
- package/dist/commands/plugin/audit.js +43 -25
- package/dist/commands/project/config.d.ts +1 -0
- package/dist/commands/project/config.js +20 -1
- package/dist/commands/project/list.d.ts +1 -1
- package/dist/commands/project/list.js +6 -6
- package/dist/commands/project/pull.js +11 -8
- package/dist/commands/project/push.js +23 -18
- package/dist/commands/project/remove.js +1 -1
- package/dist/commands/project/switch.d.ts +0 -1
- package/dist/commands/project/switch.js +16 -36
- package/dist/commands/promote.d.ts +1 -0
- package/dist/commands/promote.js +7 -3
- package/dist/commands/pull.d.ts +2 -1
- package/dist/commands/pull.js +31 -6
- package/dist/commands/push.d.ts +4 -2
- package/dist/commands/push.js +51 -19
- package/dist/commands/role/diff.d.ts +2 -0
- package/dist/commands/role/diff.js +5 -0
- package/dist/commands/role/list.d.ts +2 -0
- package/dist/commands/role/list.js +4 -0
- package/dist/commands/role/pull.d.ts +2 -0
- package/dist/commands/role/pull.js +4 -0
- package/dist/commands/role/push.d.ts +2 -0
- package/dist/commands/role/push.js +4 -0
- package/dist/commands/role/rm.d.ts +2 -0
- package/dist/commands/role/rm.js +3 -0
- package/dist/commands/role/rollback.d.ts +2 -0
- package/dist/commands/role/rollback.js +5 -0
- package/dist/commands/snippet/push.d.ts +1 -0
- package/dist/commands/snippet/push.js +24 -3
- package/dist/commands/status.d.ts +2 -3
- package/dist/commands/status.js +21 -53
- package/dist/commands/taxonomy/diff.d.ts +2 -0
- package/dist/commands/taxonomy/diff.js +5 -0
- package/dist/commands/taxonomy/list.d.ts +2 -0
- package/dist/commands/taxonomy/list.js +5 -0
- package/dist/commands/taxonomy/pull.d.ts +2 -0
- package/dist/commands/taxonomy/pull.js +4 -0
- package/dist/commands/taxonomy/push.d.ts +2 -0
- package/dist/commands/taxonomy/push.js +4 -0
- package/dist/commands/taxonomy/rm.d.ts +2 -0
- package/dist/commands/taxonomy/rm.js +3 -0
- package/dist/commands/taxonomy/rollback.d.ts +2 -0
- package/dist/commands/taxonomy/rollback.js +5 -0
- package/dist/commands/theme/template/push.d.ts +2 -0
- package/dist/commands/theme/template/push.js +26 -5
- package/dist/commands/theme/version/pull.js +6 -1
- package/dist/config/project-config.manager.d.ts +3 -3
- package/dist/config/project-config.manager.js +22 -31
- package/dist/hooks/finally.js +11 -3
- package/dist/lib/api-client.js +2 -1
- package/dist/lib/app-build.js +3 -1
- package/dist/lib/base.d.ts +2 -2
- package/dist/lib/base.js +33 -38
- package/dist/lib/declared-type-command.d.ts +35 -0
- package/dist/lib/declared-type-command.js +192 -0
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +7 -3
- package/dist/lib/diff-command.d.ts +2 -0
- package/dist/lib/diff-command.js +24 -3
- package/dist/lib/github-release.js +4 -0
- package/dist/lib/load-snippets.js +4 -0
- package/dist/lib/local-callback-server.js +3 -1
- package/dist/lib/php-files-command.d.ts +8 -0
- package/dist/lib/php-files-command.js +5 -0
- package/dist/lib/push-command.d.ts +5 -2
- package/dist/lib/push-command.js +96 -15
- package/dist/lib/put-or-create.js +3 -4
- package/dist/lib/resource-state.js +38 -85
- package/dist/lib/rollback-command.js +9 -2
- package/dist/lib/rollback-materialize.js +13 -24
- package/dist/lib/spinner.d.ts +2 -0
- package/dist/lib/spinner.js +72 -0
- package/dist/lib/temp-admin.d.ts +10 -1
- package/dist/lib/temp-admin.js +27 -2
- package/dist/lib/validate-local.js +40 -12
- package/dist/lib/wp-client.d.ts +1 -0
- package/dist/lib/wp-client.js +21 -2
- package/dist/lib/wp-site-diagnostic.js +3 -2
- package/dist/types/global-config.generated.d.ts +1 -5
- package/dist/types/project-config.generated.d.ts +28 -4
- package/dist/types/snippet.d.ts +1 -0
- package/dist/utils/composer.js +2 -1
- package/dist/utils/declared-type-format.d.ts +52 -0
- package/dist/utils/declared-type-format.js +233 -0
- package/dist/utils/resource-dirs.d.ts +9 -3
- package/dist/utils/resource-dirs.js +16 -4
- package/dist/utils/template-format.d.ts +1 -0
- package/dist/utils/theme-css.d.ts +19 -0
- package/dist/utils/theme-css.js +100 -0
- package/oclif.manifest.json +3294 -1213
- package/package.json +32 -16
- package/schemas/global-config.schema.json +2 -6
- package/schemas/project-config.schema.json +37 -7
- package/dist/commands/seo/diff.js +0 -5
- package/dist/commands/seo/list.d.ts +0 -18
- package/dist/commands/seo/list.js +0 -58
- package/dist/commands/seo/pull.d.ts +0 -24
- package/dist/commands/seo/pull.js +0 -89
- package/dist/commands/seo/push.d.ts +0 -29
- package/dist/commands/seo/push.js +0 -178
- package/dist/commands/seo/rollback.js +0 -5
- package/dist/utils/seo-format.d.ts +0 -29
- package/dist/utils/seo-format.js +0 -18
- /package/dist/commands/{seo → cpt}/diff.d.ts +0 -0
- /package/dist/commands/{seo → cpt}/rollback.d.ts +0 -0
|
@@ -26,7 +26,8 @@ export default class Push extends PushCommand {
|
|
|
26
26
|
this.log(`Pushing ACF configuration to ${url}`);
|
|
27
27
|
this.log(`ACF path: ${path}`);
|
|
28
28
|
const provider = getResourceStateProvider('acf');
|
|
29
|
-
|
|
29
|
+
// `--type` leaves the other types untouched: only the pushed ones count as overwritten.
|
|
30
|
+
const beforeState = await this.captureBeforePushState(provider, path, (id) => types.some((type) => id.startsWith(`${type}/`)));
|
|
30
31
|
for (const type of types) {
|
|
31
32
|
await this.pushType(type, path);
|
|
32
33
|
}
|
|
@@ -2,6 +2,7 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { confirmUninstall } from '../../lib/interactive.js';
|
|
5
6
|
import { PushCommand } from '../../lib/push-command.js';
|
|
6
7
|
import { isLocalEnvironment, isTimeoutError } from '../../lib/wp-client.js';
|
|
7
8
|
import { requireToInstall, toIntent } from '../../utils/composer.js';
|
|
@@ -37,8 +38,15 @@ export default class ComposerPush extends PushCommand {
|
|
|
37
38
|
this.log(`Pushing composer.json (${packageCount} ${packageCount === 1 ? 'package' : 'packages'}) to ${url}`);
|
|
38
39
|
if (lock)
|
|
39
40
|
this.log(' + composer.lock sent for drift comparison (the server resolves versions from composer.json)');
|
|
41
|
+
// The server renders composer.json from this intent alone, so a WordPress.org plugin or theme
|
|
42
|
+
// the site has and this file lacks gets uninstalled, active or not: say so before it happens.
|
|
43
|
+
const toRemove = await this.wpackagistToRemove(require);
|
|
44
|
+
if (toRemove.length > 0)
|
|
45
|
+
this.log(`Would uninstall from the site, absent from composer.json: ${toRemove.join(', ')}`);
|
|
40
46
|
if (this.dryRun)
|
|
41
47
|
return { hasLock, lockDrift: [], packageCount, status: 'dry-run' };
|
|
48
|
+
if (!(await confirmUninstall(toRemove, this.yes)))
|
|
49
|
+
this.error('Aborted.');
|
|
42
50
|
this.log('Running Composer on the server, this can take a few minutes...');
|
|
43
51
|
let response;
|
|
44
52
|
try {
|
|
@@ -74,4 +82,9 @@ export default class ComposerPush extends PushCommand {
|
|
|
74
82
|
}
|
|
75
83
|
this.log('Run `lps composer pull` to update your local composer.json and composer.lock.');
|
|
76
84
|
}
|
|
85
|
+
async wpackagistToRemove(require) {
|
|
86
|
+
const { composerJson } = await this.wp.get('loopress/v1/composer/json');
|
|
87
|
+
const current = JSON.parse(composerJson).require ?? {};
|
|
88
|
+
return Object.keys(current).filter((name) => name.startsWith('wpackagist-') && !Object.hasOwn(require, name));
|
|
89
|
+
}
|
|
77
90
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { WpClient } from '../../lib/wp-client.js';
|
|
3
|
+
type ContentCopyResult = {
|
|
4
|
+
from: string;
|
|
5
|
+
posts: number;
|
|
6
|
+
status: 'copied' | 'dry-run';
|
|
7
|
+
terms: number;
|
|
8
|
+
};
|
|
9
|
+
export default class Copy extends LoopressCommand {
|
|
10
|
+
static args: {
|
|
11
|
+
from: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
12
|
+
};
|
|
13
|
+
static baseFlags: {
|
|
14
|
+
env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
static description: string;
|
|
17
|
+
static enableJsonFlag: boolean;
|
|
18
|
+
static examples: string[];
|
|
19
|
+
static flags: {
|
|
20
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
21
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
22
|
+
};
|
|
23
|
+
run(): Promise<ContentCopyResult>;
|
|
24
|
+
protected sourceWp(from: string): WpClient;
|
|
25
|
+
private confirmReplace;
|
|
26
|
+
private copy;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { configManager } from '../../config/project-config.manager.js';
|
|
4
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
5
|
+
import { isInteractive } from '../../lib/interactive.js';
|
|
6
|
+
import { stdoutToStderr } from '../../lib/json-delegation.js';
|
|
7
|
+
import { isLocalEnvironment, LOCAL_ENVIRONMENT_NAME, WpClient } from '../../lib/wp-client.js';
|
|
8
|
+
import { pluralize } from '../../utils/pluralize.js';
|
|
9
|
+
const BATCH_SIZE = 100;
|
|
10
|
+
// Code goes up, content comes down: the content of <from> replaces the local site's, IDs
|
|
11
|
+
// unchanged, never the reverse and never into the repo. Not a `pull`: nothing is written to
|
|
12
|
+
// local files. The target is always "local", so there is no --env (this.wp is the local site),
|
|
13
|
+
// <from> names the source like `lps promote`. Terms go first, so the post batches can attach
|
|
14
|
+
// their term relationships. The plugin refuses the import on any site whose environment type is
|
|
15
|
+
// not "local", whatever this command is pointed at.
|
|
16
|
+
export default class Copy extends LoopressCommand {
|
|
17
|
+
static args = {
|
|
18
|
+
from: Args.string({ description: 'Environment to copy the content from', required: true }),
|
|
19
|
+
};
|
|
20
|
+
// Hidden, not removed: the type of LoopressCommand.baseFlags requires it. Any value other than
|
|
21
|
+
// "local" is refused in run().
|
|
22
|
+
static baseFlags = {
|
|
23
|
+
env: Flags.string({ hidden: true }),
|
|
24
|
+
};
|
|
25
|
+
static description = 'Replace the local site\'s content (posts, pages, public custom post types, terms) with the content of <from>, then push the project to the local site so tracked configuration wins. Media stay served by <from>, users, comments and orders are never copied.';
|
|
26
|
+
static enableJsonFlag = true;
|
|
27
|
+
static examples = ['$ lps content copy production', '$ lps content copy staging --dry-run'];
|
|
28
|
+
static flags = {
|
|
29
|
+
...LoopressCommand.dryRunFlag,
|
|
30
|
+
...LoopressCommand.yesFlag,
|
|
31
|
+
};
|
|
32
|
+
async run() {
|
|
33
|
+
const { args } = await this.parse(Copy);
|
|
34
|
+
const { from } = args;
|
|
35
|
+
if (!isLocalEnvironment(this.siteConfig.name)) {
|
|
36
|
+
this.error(`Content is only ever copied into the "${LOCAL_ENVIRONMENT_NAME}" environment. Run \`lps project config\` to add it if the project has none.`);
|
|
37
|
+
}
|
|
38
|
+
if (isLocalEnvironment(from)) {
|
|
39
|
+
this.error(`<from> must be another environment than "${LOCAL_ENVIRONMENT_NAME}", e.g. \`lps content copy production\`.`);
|
|
40
|
+
}
|
|
41
|
+
const source = this.sourceWp(from);
|
|
42
|
+
if (!this.dryRun)
|
|
43
|
+
await this.confirmReplace(from);
|
|
44
|
+
const info = await source.get('loopress/v1/content');
|
|
45
|
+
if (!this.dryRun) {
|
|
46
|
+
await this.wp.post('loopress/v1/content/reset', { home: info.home, uploadsUrl: info.uploadsUrl });
|
|
47
|
+
}
|
|
48
|
+
this.log(`\n→ Copying terms from ${from}...`);
|
|
49
|
+
const terms = await this.copy('terms', source);
|
|
50
|
+
this.log(`→ Copying posts from ${from}...`);
|
|
51
|
+
const posts = await this.copy('posts', source);
|
|
52
|
+
const summary = `${pluralize(posts, 'post')} and ${pluralize(terms, 'term')}`;
|
|
53
|
+
if (this.dryRun) {
|
|
54
|
+
this.log(`\n[dry-run] Would replace the local content with ${summary} from ${from}, then push the project to ${LOCAL_ENVIRONMENT_NAME}.`);
|
|
55
|
+
return { from, posts, status: 'dry-run', terms };
|
|
56
|
+
}
|
|
57
|
+
this.log(`\n→ Pushing the project to ${LOCAL_ENVIRONMENT_NAME}...`);
|
|
58
|
+
await stdoutToStderr(this.jsonEnabled(), async () => this.config.runCommand('push', ['--env', LOCAL_ENVIRONMENT_NAME, '--yes']));
|
|
59
|
+
this.log(`\nCopied ${summary} from ${from}.`);
|
|
60
|
+
return { from, posts, status: 'copied', terms };
|
|
61
|
+
}
|
|
62
|
+
// Overridable seam for tests, like `wpClient` on LoopressCommand.
|
|
63
|
+
sourceWp(from) {
|
|
64
|
+
const project = configManager.getProject(this.projectId);
|
|
65
|
+
const env = project?.environments[from];
|
|
66
|
+
if (!env) {
|
|
67
|
+
this.error(`Environment "${from}" not found. Available: ${Object.keys(project?.environments ?? {}).join(', ')}`);
|
|
68
|
+
}
|
|
69
|
+
if (!env.token)
|
|
70
|
+
this.error(`No credentials configured for ${env.url}. Run \`lps project config\` to add them.`);
|
|
71
|
+
return new WpClient(env.url, env.token, env.name);
|
|
72
|
+
}
|
|
73
|
+
async confirmReplace(from) {
|
|
74
|
+
if (this.yes)
|
|
75
|
+
return;
|
|
76
|
+
const message = `This deletes the posts, pages and terms of the "${LOCAL_ENVIRONMENT_NAME}" site and replaces them with ${from}'s.`;
|
|
77
|
+
if (!isInteractive())
|
|
78
|
+
this.error(`${message} Pass --yes to confirm.`);
|
|
79
|
+
const isProceed = await confirm({ default: true, message: `${message} Continue?` });
|
|
80
|
+
if (!isProceed)
|
|
81
|
+
this.error('Aborted.');
|
|
82
|
+
}
|
|
83
|
+
// Pages through the source, each page posted to the local site as is.
|
|
84
|
+
async copy(kind, source) {
|
|
85
|
+
let count = 0;
|
|
86
|
+
let after = 0;
|
|
87
|
+
while (after !== null) {
|
|
88
|
+
const page = await source.get(`loopress/v1/content/${kind}?after=${after}&limit=${BATCH_SIZE}`);
|
|
89
|
+
const { next, ...rows } = page;
|
|
90
|
+
count += rows[kind].length;
|
|
91
|
+
if (!this.dryRun)
|
|
92
|
+
await this.wp.post(`loopress/v1/content/${kind}`, rows);
|
|
93
|
+
after = next;
|
|
94
|
+
}
|
|
95
|
+
return count;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('cpt', {
|
|
3
|
+
description: 'Show what differs in custom post types between your local cpt/ files and a WordPress environment, or between two environments',
|
|
4
|
+
pathNoun: 'cpt directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { declaredListCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { CPT_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredListCommand(CPT_SPEC, 'List the post types registered on WordPress and where each comes from (Loopress, WordPress, ACF, CPT UI, a theme or plugin). ' +
|
|
4
|
+
'Only Loopress ones are pulled and pushed.', (count) => `${count} published`);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { declaredPullCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { CPT_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredPullCommand(CPT_SPEC, 'Pull the custom post types Loopress manages on WordPress into cpt/<slug>.json files. ' +
|
|
4
|
+
'Post types registered by a theme or another plugin are never pulled, see `lps cpt list`.');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { declaredPushCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { CPT_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredPushCommand(CPT_SPEC, 'Push local custom post types (cpt/<slug>.json, register_post_type() arguments) to WordPress. ' +
|
|
4
|
+
'Create or update only, never deletes a post type: use `lps cpt rm` for that.');
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { declaredRmCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { CPT_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredRmCommand(CPT_SPEC, 'Stop registering a Loopress custom post type on WordPress. Its posts are kept in the database, hidden until the post type is pushed again.', "Its posts are kept in the database, hidden until it's pushed again.");
|
package/dist/commands/dev.js
CHANGED
|
@@ -48,18 +48,28 @@ export default class Dev extends Command {
|
|
|
48
48
|
}
|
|
49
49
|
// Sequential on purpose (see notes technique: parallelizing push is out of scope). A failed
|
|
50
50
|
// push is logged and swallowed here, never thrown, so one bad batch can't take the watcher down.
|
|
51
|
+
// Types sharing a command run it once: templates, parts, css and blocks all rewrite the whole
|
|
52
|
+
// child theme through `theme:template:push`.
|
|
51
53
|
async pushBatch(changesByType, targets) {
|
|
54
|
+
const byCommand = new Map();
|
|
52
55
|
for (const [type, paths] of changesByType) {
|
|
53
56
|
const target = targets.find((t) => t.type === type);
|
|
54
57
|
if (!target)
|
|
55
58
|
continue;
|
|
56
|
-
|
|
59
|
+
const batch = byCommand.get(target.commandId) ?? { paths: [], types: [] };
|
|
60
|
+
batch.types.push(type);
|
|
61
|
+
batch.paths.push(...paths);
|
|
62
|
+
byCommand.set(target.commandId, batch);
|
|
63
|
+
}
|
|
64
|
+
for (const [commandId, { paths, types }] of byCommand) {
|
|
65
|
+
const label = types.join(', ');
|
|
66
|
+
this.log(`\n→ ${label} changed (${paths.map((path) => basename(path)).join(', ')}), pushing to local...`);
|
|
57
67
|
try {
|
|
58
|
-
await this.config.runCommand(
|
|
59
|
-
this.log(`✓ ${
|
|
68
|
+
await this.config.runCommand(commandId, ['--env', 'local']);
|
|
69
|
+
this.log(`✓ ${label} synced`);
|
|
60
70
|
}
|
|
61
71
|
catch (error) {
|
|
62
|
-
this.log(`✗ ${
|
|
72
|
+
this.log(`✗ ${label} failed: ${error.message}`);
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
75
|
}
|
package/dist/commands/diff.js
CHANGED
|
@@ -7,7 +7,7 @@ const RESOURCES = [...RESOURCE_STATE_PROVIDERS.map((provider) => provider.resour
|
|
|
7
7
|
export default class Diff extends DiffCommand {
|
|
8
8
|
static description = [
|
|
9
9
|
'Show what differs between your local tracked files and a WordPress environment, or between two environments.',
|
|
10
|
-
'Covers snippets, forms, ACF, API routes, Hooks, templates, template parts, pages,
|
|
10
|
+
'Covers snippets, forms, ACF, API routes, Hooks, templates, template parts, pages, Options, Menus, Theme styles, and Composer. Plugins and themes have their own `lps plugin status` / `lps theme status`.',
|
|
11
11
|
'Exit code: 0 in sync, 1 on drift, 2 when a resource could not be compared, so it doubles as a CI drift gate.',
|
|
12
12
|
].join(' ');
|
|
13
13
|
static enableJsonFlag = true;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { configManager } from '../config/project-config.manager.js';
|
|
2
2
|
import { LoopressCommand } from '../lib/base.js';
|
|
3
|
-
import {
|
|
3
|
+
import { formatRuntimeError, HOOK_FILES_RESOURCE } from '../lib/php-files-command.js';
|
|
4
|
+
import { findTempAdmins } from '../lib/temp-admin.js';
|
|
5
|
+
import { isForbiddenError, isNotFoundError } from '../lib/wp-client.js';
|
|
4
6
|
import { diagnoseWpSite } from '../lib/wp-site-diagnostic.js';
|
|
5
7
|
import { pluralize } from '../utils/pluralize.js';
|
|
6
8
|
export default class Doctor extends LoopressCommand {
|
|
@@ -32,7 +34,12 @@ export default class Doctor extends LoopressCommand {
|
|
|
32
34
|
await this.check('Loopress plugin installed (loopress/v1 endpoints)', async () => this.wp.get('loopress/v1'));
|
|
33
35
|
// wp/v2/users/me is WordPress core and requires authentication, so it validates the
|
|
34
36
|
// application password without depending on any Loopress feature.
|
|
35
|
-
await this.check('Credentials accepted (authenticated request)', async () => this.wp.get('wp/v2/users/me'));
|
|
37
|
+
const isAuthenticated = await this.check('Credentials accepted (authenticated request)', async () => this.wp.get('wp/v2/users/me'));
|
|
38
|
+
// Skipped when the credentials were just rejected: it would only repeat that same 401.
|
|
39
|
+
if (isAuthenticated) {
|
|
40
|
+
await this.checkLeftoverTempAdmins();
|
|
41
|
+
await this.checkHookErrors();
|
|
42
|
+
}
|
|
36
43
|
pluginVersion = await this.reportPluginVersion();
|
|
37
44
|
}
|
|
38
45
|
else {
|
|
@@ -72,6 +79,54 @@ export default class Doctor extends LoopressCommand {
|
|
|
72
79
|
return false;
|
|
73
80
|
}
|
|
74
81
|
}
|
|
82
|
+
// A hook that fails to load or throws while running doesn't break the site, it just quietly
|
|
83
|
+
// does nothing, and its only trace is an error_log nobody reads on managed hosting.
|
|
84
|
+
async checkHookErrors() {
|
|
85
|
+
let lookup;
|
|
86
|
+
try {
|
|
87
|
+
lookup = { files: await this.wp.get(HOOK_FILES_RESOURCE.endpoint) };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (isNotFoundError(error))
|
|
91
|
+
return; // Loopress Light has no hooks
|
|
92
|
+
lookup = { error };
|
|
93
|
+
}
|
|
94
|
+
await this.check('Hooks load and run without errors', async () => {
|
|
95
|
+
if ('error' in lookup)
|
|
96
|
+
throw lookup.error;
|
|
97
|
+
const files = Array.isArray(lookup.files) ? lookup.files : [];
|
|
98
|
+
const problems = files.flatMap((file) => [
|
|
99
|
+
...(file.error ? [`hooks/${file.filename}.php: ${file.error}`] : []),
|
|
100
|
+
...(file.runtimeError ? [`hooks/${file.filename}.php: ${formatRuntimeError(file.runtimeError)}`] : []),
|
|
101
|
+
]);
|
|
102
|
+
if (problems.length > 0)
|
|
103
|
+
throw new Error(problems.join('\n '));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// Report only, never deletes: a doctor run shouldn't change the site. A leftover is a live
|
|
107
|
+
// administrator with a password nobody holds, so it fails the run. Credentials without
|
|
108
|
+
// `list_users` can't look, which is informational, not a failure: nothing was found wrong.
|
|
109
|
+
async checkLeftoverTempAdmins() {
|
|
110
|
+
let lookup;
|
|
111
|
+
try {
|
|
112
|
+
lookup = { leftovers: await findTempAdmins(this.wp) };
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
lookup = { error };
|
|
116
|
+
}
|
|
117
|
+
if ('error' in lookup && isForbiddenError(lookup.error)) {
|
|
118
|
+
this.out('- Leftover temporary admin accounts: not checked, these credentials cannot list users.');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
await this.check('No leftover temporary admin accounts', async () => {
|
|
122
|
+
if ('error' in lookup)
|
|
123
|
+
throw lookup.error;
|
|
124
|
+
if (lookup.leftovers.length > 0) {
|
|
125
|
+
const names = lookup.leftovers.map((admin) => admin.username).join(', ');
|
|
126
|
+
throw new Error(`Found ${names}, left by an interrupted Loopress Full install. Re-run \`lps project config\` to remove them, or delete them in wp-admin > Users.`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
75
130
|
out(message) {
|
|
76
131
|
if (!this.jsonEnabled())
|
|
77
132
|
this.log(message);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import { readFile, rename } from 'node:fs/promises';
|
|
2
|
+
import { readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname, extname, join } from 'node:path';
|
|
4
4
|
import { PushCommand } from '../../lib/push-command.js';
|
|
5
5
|
import { putOrCreate } from '../../lib/put-or-create.js';
|
|
@@ -120,6 +120,9 @@ export default class Push extends PushCommand {
|
|
|
120
120
|
putEndpoint: formEndpoint,
|
|
121
121
|
});
|
|
122
122
|
const canonicalId = created ? getFormId(body) : id;
|
|
123
|
+
// The id is read from the file, not its name: without it the next push creates the form again.
|
|
124
|
+
if (created && canonicalId !== null)
|
|
125
|
+
await writeFile(filePath, JSON.stringify({ ...data, id: canonicalId }, null, 2) + '\n');
|
|
123
126
|
if (canonicalId !== null)
|
|
124
127
|
await this.ensureCanonicalFilename(filePath, canonicalId, title);
|
|
125
128
|
if (task)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('meta', {
|
|
3
|
+
description: 'Show what differs in post meta keys between your local meta/ files and a WordPress environment, or between two environments',
|
|
4
|
+
pathNoun: 'meta directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
type RegisteredMetaKey = {
|
|
3
|
+
acf: boolean;
|
|
4
|
+
conflict: boolean;
|
|
5
|
+
key: string;
|
|
6
|
+
managed: boolean;
|
|
7
|
+
postType: string;
|
|
8
|
+
showInRest: boolean;
|
|
9
|
+
single: boolean;
|
|
10
|
+
source: 'loopress' | 'other';
|
|
11
|
+
type: string;
|
|
12
|
+
};
|
|
13
|
+
export default class MetaList extends LoopressCommand {
|
|
14
|
+
static description: string;
|
|
15
|
+
static enableJsonFlag: boolean;
|
|
16
|
+
static examples: string[];
|
|
17
|
+
run(): Promise<RegisteredMetaKey[]>;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { META_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default class MetaList extends LoopressCommand {
|
|
4
|
+
static description = 'List the meta keys registered for each post type on WordPress (with register_post_meta()), and whether Loopress declares them. ' +
|
|
5
|
+
'Only Loopress ones are pulled and pushed.';
|
|
6
|
+
static enableJsonFlag = true;
|
|
7
|
+
static examples = ['$ lps meta list'];
|
|
8
|
+
async run() {
|
|
9
|
+
const keys = await this.wp.get(META_SPEC.registeredEndpoint);
|
|
10
|
+
for (const meta of keys) {
|
|
11
|
+
const source = meta.source === 'loopress' ? 'Loopress' : 'theme or plugin';
|
|
12
|
+
const rest = meta.showInRest ? ', in REST' : '';
|
|
13
|
+
const acf = meta.acf ? ', same name as an ACF field' : '';
|
|
14
|
+
this.log(`${meta.postType}.${meta.key} (${meta.type}): ${source}${rest}${acf}`);
|
|
15
|
+
if (meta.conflict) {
|
|
16
|
+
this.warn(`${meta.postType}.${meta.key}: declared in meta/ but already registered by a theme or plugin, the Loopress version is skipped`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return keys;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { declaredPullCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { META_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredPullCommand(META_SPEC, 'Pull the post meta keys Loopress manages on WordPress into meta/<post-type>.json files, never their values. ' +
|
|
4
|
+
'Keys registered by a theme or another plugin are never pulled, see `lps meta list`.');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { declaredPushCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { META_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredPushCommand(META_SPEC, 'Push local post meta keys (meta/<post-type>.json, each key mapped to its register_post_meta() arguments) to WordPress. ' +
|
|
4
|
+
'Create or update only, never deletes (use `lps meta rm`), never touches meta values.');
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { declaredRmCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { META_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredRmCommand(META_SPEC, 'Stop registering the Loopress meta keys of one post type (its meta/<post-type>.json file) on WordPress. Their values are kept in the database.', "Their values are kept in the database, hidden until it's pushed again.");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Flags, ux } from '@oclif/core';
|
|
2
2
|
import ttyTable from 'tty-table';
|
|
3
3
|
import { LoopressCommand } from '../../lib/base.js';
|
|
4
|
+
import { withSpinner } from '../../lib/spinner.js';
|
|
4
5
|
import { OPTIONS_ENDPOINT } from '../../utils/option-format.js';
|
|
5
6
|
import { pluralize } from '../../utils/pluralize.js';
|
|
6
7
|
// Same green/yellow pairing drives both SOURCE? and PLUGIN, so a row's whole "where did this come
|
|
@@ -127,14 +128,7 @@ export default class List extends LoopressCommand {
|
|
|
127
128
|
// Every call scans active plugins' own PHP source for names the naming guess missed (not
|
|
128
129
|
// opt-in, roughly 1-3s on a real site once vendor/tests/languages are pruned from the scan;
|
|
129
130
|
// see OptionsService::listOptionNames() server-side), hence the spinner unconditionally.
|
|
130
|
-
|
|
131
|
-
let options;
|
|
132
|
-
try {
|
|
133
|
-
options = await this.wp.get(OPTIONS_ENDPOINT);
|
|
134
|
-
}
|
|
135
|
-
finally {
|
|
136
|
-
ux.action.stop();
|
|
137
|
-
}
|
|
131
|
+
let options = await withSpinner('Fetching and verifying options', async () => this.wp.get(OPTIONS_ENDPOINT));
|
|
138
132
|
if (flags['no-core'])
|
|
139
133
|
options = options.filter((option) => !option.core);
|
|
140
134
|
if (options.length === 0) {
|
|
@@ -23,8 +23,6 @@ export default class Push extends PushCommand {
|
|
|
23
23
|
const { url } = this.siteConfig;
|
|
24
24
|
this.log(`Pushing tracked options to ${url}`);
|
|
25
25
|
this.log(`Options path: ${dir}`);
|
|
26
|
-
const provider = getResourceStateProvider('option');
|
|
27
|
-
const beforeState = await this.captureBeforePushState(provider, dir);
|
|
28
26
|
const tracked = await loadFiles(dir, {
|
|
29
27
|
extension: '.json',
|
|
30
28
|
onSkip: (message) => {
|
|
@@ -33,6 +31,10 @@ export default class Push extends PushCommand {
|
|
|
33
31
|
parse: parseLocalOption,
|
|
34
32
|
});
|
|
35
33
|
const { skipped, writable } = partitionByReadonly(tracked);
|
|
34
|
+
const provider = getResourceStateProvider('option');
|
|
35
|
+
// Readonly options are never written, so a site-side change to one is no reason to stop.
|
|
36
|
+
const writableNames = new Set(writable.map((option) => option.name));
|
|
37
|
+
const beforeState = await this.captureBeforePushState(provider, dir, (name) => writableNames.has(name));
|
|
36
38
|
if (skipped.length > 0) {
|
|
37
39
|
this.log(`Skipping ${pluralize(skipped.length, 'readonly option')}: ${skipped.map((option) => option.name).join(', ')}`);
|
|
38
40
|
}
|
|
@@ -29,13 +29,14 @@ export default class Push extends PushCommand {
|
|
|
29
29
|
this.error(`No page "${args.slug}" in ${path} (expected ${args.slug}.html).`);
|
|
30
30
|
}
|
|
31
31
|
this.log(`Found ${pluralize(selected.length, 'page')} to push`);
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const provider = getResourceStateProvider('page');
|
|
33
|
+
const beforeState = await this.captureBeforePushState(provider, path, args.slug === undefined ? undefined : (id) => id === args.slug);
|
|
34
34
|
const pushed = [];
|
|
35
35
|
await this.runPushTasks(selected, (page) => page.slug, async (page, task) => {
|
|
36
36
|
await this.pushPage(page, task);
|
|
37
37
|
pushed.push(page.slug);
|
|
38
38
|
});
|
|
39
|
+
await this.writeAfterPushSnapshot(provider, path, beforeState);
|
|
39
40
|
if (this.failedCount > 0) {
|
|
40
41
|
this.error(`${pluralize(this.failedCount, 'page')} failed to push.`);
|
|
41
42
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('pattern', {
|
|
3
|
+
description: 'Show what differs in block patterns between your local patterns/ files and a WordPress environment, or between two environments',
|
|
4
|
+
pathNoun: 'patterns directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { declaredListCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { PATTERN_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredListCommand(PATTERN_SPEC, 'List the block patterns registered on WordPress by Loopress, the theme and plugins (not WordPress core). Only Loopress ones are pulled and pushed.', () => '');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { declaredPullCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { PATTERN_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredPullCommand(PATTERN_SPEC, 'Pull the block patterns Loopress manages on WordPress into patterns/<slug>.html files. ' +
|
|
4
|
+
'Patterns registered by the theme or a plugin are never pulled, see `lps pattern list`.');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { declaredPushCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { PATTERN_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredPushCommand(PATTERN_SPEC, 'Push local block patterns (patterns/<slug>.html, block markup under a title/categories header) to WordPress, ' +
|
|
4
|
+
'registered as loopress/<slug>. Create or update only, never deletes a pattern (use `lps pattern rm`).');
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { declaredRmCommand } from '../../lib/declared-type-command.js';
|
|
2
|
+
import { PATTERN_SPEC } from '../../utils/declared-type-format.js';
|
|
3
|
+
export default declaredRmCommand(PATTERN_SPEC, 'Stop registering a Loopress block pattern on WordPress. A wp:pattern reference to it renders nothing, copies already inserted in posts stay.', 'A wp:pattern reference to it renders nothing until it is pushed again, copies already inserted in posts stay.');
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceRollbackCommand } from '../../lib/rollback-command.js';
|
|
2
|
+
export default resourceRollbackCommand('pattern', {
|
|
3
|
+
description: 'Restore block patterns to a snapshot taken automatically before an earlier `lps pattern push`',
|
|
4
|
+
pathNoun: 'patterns directory',
|
|
5
|
+
});
|