@loopress/cli 0.30.0 → 0.32.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 +494 -222
- package/dist/commands/acf/diff.d.ts +2 -2
- package/dist/commands/acf/diff.js +3 -1
- package/dist/commands/acf/list.d.ts +1 -0
- package/dist/commands/acf/list.js +2 -1
- package/dist/commands/acf/pull.d.ts +1 -0
- package/dist/commands/acf/pull.js +2 -1
- package/dist/commands/acf/push.d.ts +7 -2
- package/dist/commands/acf/push.js +14 -5
- package/dist/commands/acf/rollback.d.ts +2 -2
- package/dist/commands/acf/rollback.js +3 -1
- package/dist/commands/composer/pull.js +1 -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/diff.d.ts +1 -0
- package/dist/commands/diff.js +13 -2
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +29 -2
- package/dist/commands/form/list.js +1 -1
- package/dist/commands/form/pull.js +1 -1
- package/dist/commands/form/push.d.ts +2 -1
- package/dist/commands/form/push.js +6 -3
- package/dist/commands/init.js +0 -23
- package/dist/commands/menu/push.d.ts +2 -1
- package/dist/commands/menu/push.js +1 -1
- package/dist/commands/option/push.d.ts +2 -1
- package/dist/commands/option/push.js +5 -3
- package/dist/commands/page/push.d.ts +4 -1
- package/dist/commands/page/push.js +43 -10
- package/dist/commands/pattern/push.js +1 -1
- package/dist/commands/pattern/rm.js +1 -1
- package/dist/commands/plugin/audit.js +42 -25
- package/dist/commands/plugin/push.js +1 -1
- package/dist/commands/plugin/status.js +1 -1
- 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 +37 -5
- package/dist/commands/push.d.ts +6 -2
- package/dist/commands/push.js +64 -19
- package/dist/commands/snippet/list.js +1 -1
- package/dist/commands/snippet/pull.js +1 -1
- package/dist/commands/snippet/push.d.ts +3 -1
- package/dist/commands/snippet/push.js +26 -5
- package/dist/commands/theme/pull.js +1 -1
- package/dist/commands/theme/push.d.ts +1 -0
- package/dist/commands/theme/push.js +3 -3
- package/dist/commands/theme/style/push.d.ts +2 -1
- package/dist/commands/theme/style/push.js +4 -5
- package/dist/commands/theme/template/list.js +1 -1
- package/dist/commands/theme/template/push.d.ts +11 -1
- package/dist/commands/theme/template/push.js +34 -5
- package/dist/commands/theme/version/pull.js +6 -1
- package/dist/commands/theme/version/push.js +1 -1
- package/dist/lib/base.d.ts +1 -0
- package/dist/lib/base.js +3 -0
- package/dist/lib/declared-type-command.d.ts +2 -1
- package/dist/lib/declared-type-command.js +1 -1
- package/dist/lib/diff-command.d.ts +3 -0
- package/dist/lib/diff-command.js +34 -5
- package/dist/lib/load-snippets.js +4 -0
- package/dist/lib/php-files-command.d.ts +11 -1
- package/dist/lib/php-files-command.js +39 -19
- package/dist/lib/plugin-detection.d.ts +1 -0
- package/dist/lib/plugin-detection.js +4 -0
- package/dist/lib/push-command.d.ts +21 -2
- package/dist/lib/push-command.js +105 -6
- package/dist/lib/resource-state.js +5 -5
- package/dist/lib/rollback-command.js +9 -2
- package/dist/lib/validate-local.js +21 -8
- package/dist/lib/wp-client.d.ts +1 -0
- package/dist/lib/wp-client.js +27 -1
- package/dist/types/snippet.d.ts +1 -0
- package/dist/utils/acf-format.d.ts +5 -0
- package/dist/utils/acf-format.js +29 -0
- package/dist/utils/php-top-level.d.ts +2 -0
- package/dist/utils/php-top-level.js +148 -0
- package/dist/utils/resource-dirs.d.ts +1 -0
- package/dist/utils/resource-dirs.js +7 -1
- package/dist/utils/template-format.d.ts +1 -0
- package/dist/utils/theme-styles-format.d.ts +2 -0
- package/dist/utils/theme-styles-format.js +32 -4
- package/oclif.manifest.json +1132 -919
- package/package.json +6 -3
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const
|
|
2
|
-
export default
|
|
1
|
+
declare const Command: typeof import("../../lib/diff-command.js").DiffCommand;
|
|
2
|
+
export default Command;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
-
|
|
2
|
+
const Command = resourceDiffCommand('acf', {
|
|
3
3
|
description: 'Show what differs in ACF field groups, post types, taxonomies, and options pages between your local files and a WordPress environment, or between two environments',
|
|
4
4
|
pathNoun: 'ACF directory',
|
|
5
5
|
});
|
|
6
|
+
Command.aliases = ['scf:diff'];
|
|
7
|
+
export default Command;
|
|
@@ -2,7 +2,8 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import { LoopressCommand } from '../../lib/base.js';
|
|
3
3
|
import { ACF_OBJECT_TYPES, acfEndpoint, getAcfKey } from '../../utils/acf-format.js';
|
|
4
4
|
export default class List extends LoopressCommand {
|
|
5
|
-
static
|
|
5
|
+
static aliases = ['scf:list'];
|
|
6
|
+
static description = 'List ACF field groups, post types, taxonomies, and options pages currently on WordPress';
|
|
6
7
|
static enableJsonFlag = true;
|
|
7
8
|
static examples = ['$ lps acf list', '$ lps acf list --type field-groups'];
|
|
8
9
|
static flags = {
|
|
@@ -5,10 +5,11 @@ import { LoopressCommand } from '../../lib/base.js';
|
|
|
5
5
|
import { basenameKey, findOrphanedFiles } from '../../lib/find-orphaned-files.js';
|
|
6
6
|
import { ACF_OBJECT_TYPES, acfEndpoint, getAcfKey } from '../../utils/acf-format.js';
|
|
7
7
|
export default class Pull extends LoopressCommand {
|
|
8
|
+
static aliases = ['scf:pull'];
|
|
8
9
|
static args = {
|
|
9
10
|
path: Args.string({ description: 'Path to ACF directory (overrides project config)' }),
|
|
10
11
|
};
|
|
11
|
-
static description = 'Pull ACF field groups, post types, taxonomies, and options pages from WordPress';
|
|
12
|
+
static description = 'Pull ACF field groups, post types, taxonomies, and options pages from WordPress into local files';
|
|
12
13
|
static enableJsonFlag = true;
|
|
13
14
|
static examples = ['$ lps acf pull', '$ lps acf pull --type field-groups'];
|
|
14
15
|
static flags = {
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { PushCommand } from '../../lib/push-command.js';
|
|
2
|
-
import { type AcfObjectType } from '../../utils/acf-format.js';
|
|
1
|
+
import { type PushChange, PushCommand } from '../../lib/push-command.js';
|
|
2
|
+
import { acfFieldChanges, type AcfObjectType } from '../../utils/acf-format.js';
|
|
3
|
+
type AcfChange = PushChange & {
|
|
4
|
+
fields?: ReturnType<typeof acfFieldChanges>;
|
|
5
|
+
};
|
|
3
6
|
type AcfPushResult = {
|
|
7
|
+
changes?: AcfChange[];
|
|
4
8
|
path: string;
|
|
5
9
|
status: 'dry-run' | 'success';
|
|
6
10
|
types: AcfObjectType[];
|
|
7
11
|
};
|
|
8
12
|
export default class Push extends PushCommand {
|
|
13
|
+
static aliases: string[];
|
|
9
14
|
static args: {
|
|
10
15
|
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
11
16
|
};
|
|
@@ -4,13 +4,14 @@ import { loadFiles } from '../../lib/load-files.js';
|
|
|
4
4
|
import { PushCommand } from '../../lib/push-command.js';
|
|
5
5
|
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
6
6
|
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
7
|
-
import { ACF_OBJECT_TYPES, acfEndpoint, acfObjectEndpoint, getAcfKey } from '../../utils/acf-format.js';
|
|
7
|
+
import { ACF_OBJECT_TYPES, acfEndpoint, acfFieldChanges, acfObjectEndpoint, getAcfKey } from '../../utils/acf-format.js';
|
|
8
8
|
import { pluralize } from '../../utils/pluralize.js';
|
|
9
9
|
export default class Push extends PushCommand {
|
|
10
|
+
static aliases = ['scf:push'];
|
|
10
11
|
static args = {
|
|
11
12
|
path: Args.string({ description: 'Path to ACF directory (overrides project config)' }),
|
|
12
13
|
};
|
|
13
|
-
static description = 'Push ACF field groups, post types, taxonomies, and options pages to WordPress';
|
|
14
|
+
static description = 'Push local ACF field groups, post types, taxonomies, and options pages to WordPress';
|
|
14
15
|
static enableJsonFlag = true;
|
|
15
16
|
static examples = ['$ lps acf push', '$ lps acf push --type field-groups'];
|
|
16
17
|
static flags = {
|
|
@@ -26,7 +27,8 @@ export default class Push extends PushCommand {
|
|
|
26
27
|
this.log(`Pushing ACF configuration to ${url}`);
|
|
27
28
|
this.log(`ACF path: ${path}`);
|
|
28
29
|
const provider = getResourceStateProvider('acf');
|
|
29
|
-
|
|
30
|
+
// `--type` leaves the other types untouched: only the pushed ones count as overwritten.
|
|
31
|
+
const beforeState = await this.captureBeforePushState(provider, path, (id) => types.some((type) => id.startsWith(`${type}/`)));
|
|
30
32
|
for (const type of types) {
|
|
31
33
|
await this.pushType(type, path);
|
|
32
34
|
}
|
|
@@ -34,8 +36,15 @@ export default class Push extends PushCommand {
|
|
|
34
36
|
if (this.failedCount > 0) {
|
|
35
37
|
this.error(`${pluralize(this.failedCount, 'ACF object')} failed to push.`);
|
|
36
38
|
}
|
|
37
|
-
if (this.dryRun)
|
|
38
|
-
|
|
39
|
+
if (this.dryRun) {
|
|
40
|
+
const states = this.previewStates;
|
|
41
|
+
const changes = this.previewChanges.map((change) => {
|
|
42
|
+
if (!states || change.status !== 'changed' || !change.id.startsWith('field-groups/'))
|
|
43
|
+
return change;
|
|
44
|
+
return { ...change, fields: acfFieldChanges(states.remote.get(change.id), states.local.get(change.id)) };
|
|
45
|
+
});
|
|
46
|
+
return { changes, path, status: 'dry-run', types };
|
|
47
|
+
}
|
|
39
48
|
await this.recordSuccess();
|
|
40
49
|
this.log('All ACF objects pushed.');
|
|
41
50
|
return { path, status: 'success', types };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const
|
|
2
|
-
export default
|
|
1
|
+
declare const Command: typeof import("../../lib/base.js").LoopressCommand;
|
|
2
|
+
export default Command;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { resourceRollbackCommand } from '../../lib/rollback-command.js';
|
|
2
|
-
|
|
2
|
+
const Command = resourceRollbackCommand('acf', {
|
|
3
3
|
description: 'Restore ACF field groups, post types, taxonomies, and options pages to a snapshot taken automatically before an earlier `lps acf push`',
|
|
4
4
|
pathNoun: 'ACF directory',
|
|
5
5
|
});
|
|
6
|
+
Command.aliases = ['scf:rollback'];
|
|
7
|
+
export default Command;
|
|
@@ -10,7 +10,7 @@ function isMissingComposerLock(error) {
|
|
|
10
10
|
return isApplicative404(error, 'composer.lock not found');
|
|
11
11
|
}
|
|
12
12
|
export default class ComposerPull extends LoopressCommand {
|
|
13
|
-
static description = 'Pull composer.json and composer.lock from WordPress';
|
|
13
|
+
static description = 'Pull composer.json and composer.lock from WordPress into local files';
|
|
14
14
|
static enableJsonFlag = true;
|
|
15
15
|
static examples = ['$ lps composer pull', '$ lps composer pull --dry-run'];
|
|
16
16
|
static flags = {
|
|
@@ -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
|
+
}
|
package/dist/commands/diff.d.ts
CHANGED
package/dist/commands/diff.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import { DiffCommand } from '../lib/diff-command.js';
|
|
3
3
|
import { RESOURCE_STATE_PROVIDERS } from '../lib/resource-state.js';
|
|
4
|
+
import { projectUses, resolveResourceDir } from '../utils/resource-dirs.js';
|
|
5
|
+
import { globalStylesFiles } from '../utils/theme-styles-format.js';
|
|
4
6
|
// Everything `lps diff` can compare, in report order. Composer isn't a directory-backed
|
|
5
7
|
// provider (see resource-state.ts) so it's named here explicitly.
|
|
6
8
|
const RESOURCES = [...RESOURCE_STATE_PROVIDERS.map((provider) => provider.resource), 'composer'];
|
|
7
9
|
export default class Diff extends DiffCommand {
|
|
8
10
|
static description = [
|
|
9
11
|
'Show what differs between your local tracked files and a WordPress environment, or between two environments.',
|
|
10
|
-
'Covers snippets, forms, ACF, API routes,
|
|
12
|
+
'Covers snippets, forms, ACF, API routes, hooks, templates, template parts, pages, menus, custom post types, taxonomies, post meta, roles, block patterns, options, theme styles, and Composer. Plugins and themes have their own `lps plugin status` / `lps theme status`.',
|
|
11
13
|
'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
14
|
].join(' ');
|
|
13
15
|
static enableJsonFlag = true;
|
|
@@ -27,12 +29,21 @@ export default class Diff extends DiffCommand {
|
|
|
27
29
|
const { flags } = await this.parse(Diff);
|
|
28
30
|
const sides = this.resolveSides(flags.against);
|
|
29
31
|
const selected = this.selectResources(flags.only, flags.skip);
|
|
32
|
+
const providers = RESOURCE_STATE_PROVIDERS.filter((provider) => selected.has(provider.resource));
|
|
30
33
|
const targets = [
|
|
31
|
-
...
|
|
34
|
+
...(await Promise.all(providers.map(async (provider) => ({ ...this.providerTarget(provider, sides), optional: !(await this.projectUses(provider)) })))),
|
|
32
35
|
...(selected.has('composer') ? [this.composerTarget(sides)] : []),
|
|
33
36
|
];
|
|
34
37
|
return this.report(targets, sides);
|
|
35
38
|
}
|
|
39
|
+
// Tells apart a resource the project doesn't use from one it does, for the ones a site can lack:
|
|
40
|
+
// a plugin-backed resource without its directory, Global Styles without a tracked file.
|
|
41
|
+
async projectUses(provider) {
|
|
42
|
+
if (provider.dirKind === 'themeStyles') {
|
|
43
|
+
return (await globalStylesFiles(resolveResourceDir('themeStyles', this.localConfig))).length > 0;
|
|
44
|
+
}
|
|
45
|
+
return projectUses(provider.dirKind, this.localConfig);
|
|
46
|
+
}
|
|
36
47
|
// `--only` narrows the default set, `--skip` then removes from whatever that set is, same
|
|
37
48
|
// precedence a combined `--only=a,b --skip=b` reads as (mirrors dev-watch's resolveResourceTypes).
|
|
38
49
|
selectResources(only, skip) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { configManager } from '../config/project-config.manager.js';
|
|
2
2
|
import { LoopressCommand } from '../lib/base.js';
|
|
3
|
+
import { formatRuntimeError, HOOK_FILES_RESOURCE } from '../lib/php-files-command.js';
|
|
3
4
|
import { findTempAdmins } from '../lib/temp-admin.js';
|
|
4
5
|
import { isForbiddenError, isNotFoundError } from '../lib/wp-client.js';
|
|
5
6
|
import { diagnoseWpSite } from '../lib/wp-site-diagnostic.js';
|
|
6
7
|
import { pluralize } from '../utils/pluralize.js';
|
|
7
8
|
export default class Doctor extends LoopressCommand {
|
|
8
|
-
static description = 'Diagnose connectivity, plugin and
|
|
9
|
+
static description = 'Diagnose connectivity, plugin, credential and hook problems for the targeted environment';
|
|
9
10
|
static enableJsonFlag = true;
|
|
10
11
|
static examples = ['$ lps doctor', '$ lps doctor --env production'];
|
|
11
12
|
checks = [];
|
|
@@ -35,8 +36,10 @@ export default class Doctor extends LoopressCommand {
|
|
|
35
36
|
// application password without depending on any Loopress feature.
|
|
36
37
|
const isAuthenticated = await this.check('Credentials accepted (authenticated request)', async () => this.wp.get('wp/v2/users/me'));
|
|
37
38
|
// Skipped when the credentials were just rejected: it would only repeat that same 401.
|
|
38
|
-
if (isAuthenticated)
|
|
39
|
+
if (isAuthenticated) {
|
|
39
40
|
await this.checkLeftoverTempAdmins();
|
|
41
|
+
await this.checkHookErrors();
|
|
42
|
+
}
|
|
40
43
|
pluginVersion = await this.reportPluginVersion();
|
|
41
44
|
}
|
|
42
45
|
else {
|
|
@@ -76,6 +79,30 @@ export default class Doctor extends LoopressCommand {
|
|
|
76
79
|
return false;
|
|
77
80
|
}
|
|
78
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
|
+
}
|
|
79
106
|
// Report only, never deletes: a doctor run shouldn't change the site. A leftover is a live
|
|
80
107
|
// administrator with a password nobody holds, so it fails the run. Credentials without
|
|
81
108
|
// `list_users` can't look, which is informational, not a failure: nothing was found wrong.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
2
|
import { FORM_ENDPOINT, getFormId, getFormTitle } from '../../utils/form-format.js';
|
|
3
3
|
export default class List extends LoopressCommand {
|
|
4
|
-
static description = 'List forms
|
|
4
|
+
static description = 'List forms currently on WordPress';
|
|
5
5
|
static enableJsonFlag = true;
|
|
6
6
|
static examples = ['$ lps form list'];
|
|
7
7
|
async run() {
|
|
@@ -10,7 +10,7 @@ export default class Pull extends LoopressCommand {
|
|
|
10
10
|
static args = {
|
|
11
11
|
path: Args.string({ description: 'Path to forms directory (overrides project config)' }),
|
|
12
12
|
};
|
|
13
|
-
static description = 'Pull forms from WordPress';
|
|
13
|
+
static description = 'Pull forms from WordPress into local files';
|
|
14
14
|
static enableJsonFlag = true;
|
|
15
15
|
static examples = ['$ lps form pull'];
|
|
16
16
|
static flags = {
|
|
@@ -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';
|
|
@@ -13,7 +13,7 @@ export default class Push extends PushCommand {
|
|
|
13
13
|
static args = {
|
|
14
14
|
path: Args.string({ description: 'Path to forms directory (overrides project config)' }),
|
|
15
15
|
};
|
|
16
|
-
static description = 'Push
|
|
16
|
+
static description = 'Push local form files to WordPress. Local files created or updated remotely are renamed on disk to the `<id>-<slug>.json` convention.';
|
|
17
17
|
static enableJsonFlag = true;
|
|
18
18
|
static examples = ['$ lps form push', '$ lps form push --allow-notifications'];
|
|
19
19
|
static flags = {
|
|
@@ -42,7 +42,7 @@ export default class Push extends PushCommand {
|
|
|
42
42
|
this.error(`${pluralize(this.failedCount, 'form')} failed to push.`);
|
|
43
43
|
}
|
|
44
44
|
if (this.dryRun)
|
|
45
|
-
return { path, status: 'dry-run' };
|
|
45
|
+
return { changes: this.previewChanges, path, status: 'dry-run' };
|
|
46
46
|
await this.recordSuccess();
|
|
47
47
|
this.log('All forms pushed.');
|
|
48
48
|
return { path, status: 'success' };
|
|
@@ -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)
|
package/dist/commands/init.js
CHANGED
|
@@ -5,12 +5,6 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { configManager } from '../config/project-config.manager.js';
|
|
6
6
|
import { isInteractive } from '../lib/interactive.js';
|
|
7
7
|
import { ensureLfGitattributes, writeLocalConfig } from '../utils/loopress-config.js';
|
|
8
|
-
// WordPress.org slugs for the two snippet plugins the Loopress WordPress plugin supports
|
|
9
|
-
// (see SnippetModule.php, which wires up both providers and auto-detects the active one).
|
|
10
|
-
const SNIPPET_PROVIDERS = [
|
|
11
|
-
{ name: 'Code Snippets', slug: 'code-snippets' },
|
|
12
|
-
{ name: 'WPCode', slug: 'insert-headers-and-footers' },
|
|
13
|
-
];
|
|
14
8
|
export default class Init extends Command {
|
|
15
9
|
static description = 'Initialize a loopress.json config file in the current directory';
|
|
16
10
|
static examples = ['$ lps init'];
|
|
@@ -35,31 +29,14 @@ export default class Init extends Command {
|
|
|
35
29
|
default: '.',
|
|
36
30
|
message: 'Root directory',
|
|
37
31
|
});
|
|
38
|
-
const providerChoice = await select({
|
|
39
|
-
choices: [...SNIPPET_PROVIDERS.map((p) => ({ name: p.name, value: p.slug })), { name: 'None / already installed', value: '__none__' }],
|
|
40
|
-
message: 'Snippet provider',
|
|
41
|
-
});
|
|
42
32
|
// Resource directories are left out on purpose: every command falls back to its default
|
|
43
33
|
// (utils/resource-dirs.ts), and a custom name is a one-line edit in loopress.json.
|
|
44
34
|
const config = { projectId, rootDir };
|
|
45
35
|
await writeLocalConfig(config);
|
|
46
36
|
const isGitattributesUpdated = await ensureLfGitattributes();
|
|
47
|
-
let isProviderAdded = false;
|
|
48
|
-
if (providerChoice !== '__none__') {
|
|
49
|
-
try {
|
|
50
|
-
await this.config.runCommand('plugin:add', [providerChoice]);
|
|
51
|
-
isProviderAdded = true;
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
this.warn(error.message);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
37
|
this.log(`\n✓ loopress.json created`);
|
|
58
38
|
this.log(` Project: ${projectLabel}`);
|
|
59
39
|
this.log(` Root: ${rootDir}`);
|
|
60
|
-
if (isProviderAdded) {
|
|
61
|
-
this.log(` Plugin: ${providerChoice}`);
|
|
62
|
-
}
|
|
63
40
|
if (isGitattributesUpdated) {
|
|
64
41
|
this.log('✓ .gitattributes: LF line endings, so files match across macOS, Linux and Windows');
|
|
65
42
|
}
|
|
@@ -36,7 +36,7 @@ export default class Push extends PushCommand {
|
|
|
36
36
|
this.error(`${pluralize(this.failedCount, 'menu')} failed to push.`);
|
|
37
37
|
}
|
|
38
38
|
if (this.dryRun)
|
|
39
|
-
return { path, status: 'dry-run' };
|
|
39
|
+
return { changes: this.previewChanges, path, status: 'dry-run' };
|
|
40
40
|
await this.recordSuccess();
|
|
41
41
|
this.log('All nav menus pushed.');
|
|
42
42
|
return { path, status: 'success' };
|
|
@@ -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
|
}
|
|
@@ -48,7 +50,7 @@ export default class Push extends PushCommand {
|
|
|
48
50
|
}
|
|
49
51
|
const skippedNames = skipped.map((option) => option.name);
|
|
50
52
|
if (this.dryRun)
|
|
51
|
-
return { pushed, skipped: skippedNames, status: 'dry-run' };
|
|
53
|
+
return { changes: this.previewChanges, pushed, skipped: skippedNames, status: 'dry-run' };
|
|
52
54
|
await this.recordSuccess();
|
|
53
55
|
this.log('All options pushed.');
|
|
54
56
|
return { pushed, skipped: skippedNames, status: 'success' };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { PushCommand } from '../../lib/push-command.js';
|
|
1
|
+
import { type PushChange, PushCommand } from '../../lib/push-command.js';
|
|
2
2
|
type PushResult = {
|
|
3
|
+
changes?: PushChange[];
|
|
3
4
|
pushed: string[];
|
|
4
5
|
status: 'dry-run' | 'success';
|
|
5
6
|
};
|
|
@@ -14,7 +15,9 @@ export default class Push extends PushCommand {
|
|
|
14
15
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
16
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
17
|
};
|
|
18
|
+
private readonly unmanaged;
|
|
17
19
|
run(): Promise<PushResult>;
|
|
20
|
+
private adoptUnmanaged;
|
|
18
21
|
private pushPage;
|
|
19
22
|
}
|
|
20
23
|
export {};
|