@loopress/cli 0.28.0 → 0.29.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 +370 -203
- package/bin/dev.js +0 -0
- package/dist/commands/acf/pull.d.ts +9 -1
- package/dist/commands/acf/pull.js +2 -0
- package/dist/commands/acf/push.d.ts +9 -1
- package/dist/commands/acf/push.js +3 -1
- package/dist/commands/api/publish.js +1 -1
- package/dist/commands/app/push.js +1 -1
- package/dist/commands/composer/push.js +3 -3
- package/dist/commands/form/pull.d.ts +9 -1
- package/dist/commands/form/pull.js +3 -1
- package/dist/commands/form/push.d.ts +7 -1
- package/dist/commands/form/push.js +3 -1
- package/dist/commands/init.js +6 -38
- package/dist/commands/menu/pull.d.ts +7 -1
- package/dist/commands/menu/pull.js +2 -0
- package/dist/commands/menu/push.d.ts +7 -1
- package/dist/commands/menu/push.js +3 -1
- package/dist/commands/option/pull.js +4 -4
- package/dist/commands/option/push.js +2 -0
- package/dist/commands/page/push.js +3 -0
- package/dist/commands/plugin/pull.d.ts +4 -0
- package/dist/commands/plugin/pull.js +56 -10
- package/dist/commands/plugin/push.js +5 -4
- package/dist/commands/push.js +5 -4
- package/dist/commands/seo/pull.d.ts +8 -1
- package/dist/commands/seo/pull.js +2 -0
- package/dist/commands/seo/push.d.ts +7 -1
- package/dist/commands/seo/push.js +3 -1
- package/dist/commands/snippet/publish.js +1 -1
- package/dist/commands/snippet/pull.js +1 -1
- package/dist/commands/snippet/push.js +1 -1
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +4 -4
- package/dist/commands/theme/pull.d.ts +6 -10
- package/dist/commands/theme/pull.js +30 -32
- package/dist/commands/theme/push.d.ts +8 -9
- package/dist/commands/theme/push.js +54 -111
- package/dist/commands/theme/style/pull.d.ts +7 -1
- package/dist/commands/theme/style/pull.js +3 -1
- package/dist/commands/theme/style/push.d.ts +8 -1
- package/dist/commands/theme/style/push.js +4 -1
- package/dist/commands/{template → theme/template}/diff.d.ts +1 -1
- package/dist/commands/{template → theme/template}/diff.js +3 -3
- package/dist/commands/{template → theme/template}/list.d.ts +2 -2
- package/dist/commands/{template → theme/template}/list.js +5 -5
- package/dist/commands/{template → theme/template}/push.d.ts +1 -1
- package/dist/commands/{template → theme/template}/push.js +8 -6
- package/dist/commands/theme/version/pull.d.ts +23 -0
- package/dist/commands/theme/version/pull.js +43 -0
- package/dist/commands/theme/version/push.d.ts +22 -0
- package/dist/commands/theme/version/push.js +123 -0
- package/dist/hooks/finally.d.ts +2 -1
- package/dist/hooks/finally.js +27 -4
- package/dist/lib/app-manifest.js +2 -2
- package/dist/lib/base.d.ts +1 -0
- package/dist/lib/base.js +3 -0
- package/dist/lib/dev-watch.js +2 -2
- package/dist/lib/diff-state.js +1 -0
- package/dist/lib/github-release.js +0 -1
- package/dist/lib/local-callback-server.d.ts +12 -6
- package/dist/lib/local-callback-server.js +21 -22
- package/dist/lib/php-files-command.js +3 -1
- package/dist/lib/push-command.d.ts +5 -0
- package/dist/lib/push-command.js +30 -8
- package/dist/lib/resource-state.js +7 -6
- package/dist/lib/snapshot-store.js +2 -2
- package/dist/lib/wp-authorize-flow.d.ts +10 -17
- package/dist/lib/wp-authorize-flow.js +24 -28
- package/dist/lib/wp-site-diagnostic.js +37 -0
- package/dist/utils/composer.d.ts +3 -1
- package/dist/utils/composer.js +14 -6
- package/dist/utils/option-format.d.ts +3 -1
- package/dist/utils/option-format.js +14 -3
- package/dist/utils/page-format.d.ts +5 -1
- package/dist/utils/page-format.js +7 -3
- package/dist/utils/plugin-sync.js +1 -1
- package/dist/utils/resource-dirs.js +2 -2
- package/dist/utils/template-format.d.ts +2 -1
- package/dist/utils/template-format.js +10 -3
- package/oclif.manifest.json +937 -361
- package/package.json +22 -16
package/bin/dev.js
CHANGED
|
File without changes
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { type AcfObjectType } from '../../utils/acf-format.js';
|
|
3
|
+
type AcfPullResult = {
|
|
4
|
+
path: string;
|
|
5
|
+
status: 'dry-run' | 'success';
|
|
6
|
+
types: AcfObjectType[];
|
|
7
|
+
};
|
|
2
8
|
export default class Pull extends LoopressCommand {
|
|
3
9
|
static args: {
|
|
4
10
|
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
11
|
};
|
|
6
12
|
static description: string;
|
|
13
|
+
static enableJsonFlag: boolean;
|
|
7
14
|
static examples: string[];
|
|
8
15
|
static flags: {
|
|
9
16
|
type: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
17
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
18
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
19
|
};
|
|
13
|
-
run(): Promise<
|
|
20
|
+
run(): Promise<AcfPullResult>;
|
|
14
21
|
private pullType;
|
|
15
22
|
}
|
|
23
|
+
export {};
|
|
@@ -9,6 +9,7 @@ export default class Pull extends LoopressCommand {
|
|
|
9
9
|
path: Args.string({ description: 'Path to ACF directory (overrides project config)' }),
|
|
10
10
|
};
|
|
11
11
|
static description = 'Pull ACF field groups, post types, taxonomies, and options pages from WordPress';
|
|
12
|
+
static enableJsonFlag = true;
|
|
12
13
|
static examples = ['$ lps acf pull', '$ lps acf pull --type field-groups'];
|
|
13
14
|
static flags = {
|
|
14
15
|
...LoopressCommand.dryRunFlag,
|
|
@@ -25,6 +26,7 @@ export default class Pull extends LoopressCommand {
|
|
|
25
26
|
for (const type of types) {
|
|
26
27
|
await this.pullType(type, path);
|
|
27
28
|
}
|
|
29
|
+
return { path, status: this.dryRun ? 'dry-run' : 'success', types };
|
|
28
30
|
}
|
|
29
31
|
async pullType(type, basePath) {
|
|
30
32
|
const dir = join(basePath, type);
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { PushCommand } from '../../lib/push-command.js';
|
|
2
|
+
import { type AcfObjectType } from '../../utils/acf-format.js';
|
|
3
|
+
type AcfPushResult = {
|
|
4
|
+
path: string;
|
|
5
|
+
status: 'dry-run' | 'success';
|
|
6
|
+
types: AcfObjectType[];
|
|
7
|
+
};
|
|
2
8
|
export default class Push extends PushCommand {
|
|
3
9
|
static args: {
|
|
4
10
|
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
11
|
};
|
|
6
12
|
static description: string;
|
|
13
|
+
static enableJsonFlag: boolean;
|
|
7
14
|
static examples: string[];
|
|
8
15
|
static flags: {
|
|
9
16
|
type: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
17
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
18
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
19
|
};
|
|
13
|
-
run(): Promise<
|
|
20
|
+
run(): Promise<AcfPushResult>;
|
|
14
21
|
private currentRevision;
|
|
15
22
|
private loadObjects;
|
|
16
23
|
private pushObject;
|
|
17
24
|
private pushType;
|
|
18
25
|
}
|
|
26
|
+
export {};
|
|
@@ -11,6 +11,7 @@ export default class Push extends PushCommand {
|
|
|
11
11
|
path: Args.string({ description: 'Path to ACF directory (overrides project config)' }),
|
|
12
12
|
};
|
|
13
13
|
static description = 'Push ACF field groups, post types, taxonomies, and options pages to WordPress';
|
|
14
|
+
static enableJsonFlag = true;
|
|
14
15
|
static examples = ['$ lps acf push', '$ lps acf push --type field-groups'];
|
|
15
16
|
static flags = {
|
|
16
17
|
...PushCommand.dryRunFlag,
|
|
@@ -34,9 +35,10 @@ export default class Push extends PushCommand {
|
|
|
34
35
|
this.error(`${pluralize(this.failedCount, 'ACF object')} failed to push.`);
|
|
35
36
|
}
|
|
36
37
|
if (this.dryRun)
|
|
37
|
-
return;
|
|
38
|
+
return { path, status: 'dry-run', types };
|
|
38
39
|
await this.recordSuccess();
|
|
39
40
|
this.log('All ACF objects pushed.');
|
|
41
|
+
return { path, status: 'success', types };
|
|
40
42
|
}
|
|
41
43
|
async currentRevision(type, key) {
|
|
42
44
|
try {
|
|
@@ -16,7 +16,7 @@ export default class Publish extends Command {
|
|
|
16
16
|
path: Args.string({ description: 'Path to api directory (overrides project config)' }),
|
|
17
17
|
};
|
|
18
18
|
static description = 'Publish custom API routes to your Loopress account so they can be deployed to other projects. Does not touch any WordPress site.';
|
|
19
|
-
static examples = ['$ lps api publish', '$ lps api publish
|
|
19
|
+
static examples = ['$ lps api publish', '$ lps api publish ./api'];
|
|
20
20
|
async run() {
|
|
21
21
|
const { args } = await this.parse(Publish);
|
|
22
22
|
const { apiProjectId, localConfig, token } = await resolveLinkedProject((message) => this.error(message));
|
|
@@ -63,7 +63,7 @@ export default class Push extends PushCommand {
|
|
|
63
63
|
.filter((entry) => entry.isDirectory())
|
|
64
64
|
.map((entry) => entry.name)
|
|
65
65
|
.filter((name) => existsSync(join(path, name, APP_CONFIG_FILENAME)))
|
|
66
|
-
.
|
|
66
|
+
.toSorted((a, b) => a.localeCompare(b));
|
|
67
67
|
if (only) {
|
|
68
68
|
if (!names.includes(only)) {
|
|
69
69
|
this.error(`No app "${only}" in ${path} (looked for ${join(only, APP_CONFIG_FILENAME)}).`);
|
|
@@ -3,8 +3,8 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { PushCommand } from '../../lib/push-command.js';
|
|
6
|
-
import { isTimeoutError } from '../../lib/wp-client.js';
|
|
7
|
-
import { toIntent } from '../../utils/composer.js';
|
|
6
|
+
import { isLocalEnvironment, isTimeoutError } from '../../lib/wp-client.js';
|
|
7
|
+
import { requireToInstall, toIntent } from '../../utils/composer.js';
|
|
8
8
|
import { parseCollisions, SYNC_TIMEOUT_MS, } from '../../utils/plugin-sync.js';
|
|
9
9
|
export default class ComposerPush extends PushCommand {
|
|
10
10
|
static description = 'Push composer.json to WordPress and run Composer to resolve and install dependencies';
|
|
@@ -25,7 +25,7 @@ export default class ComposerPush extends PushCommand {
|
|
|
25
25
|
this.error(`No composer.json found at ${composerJsonPath}. Run \`lps composer init\` first.`);
|
|
26
26
|
}
|
|
27
27
|
const parsed = JSON.parse(await readFile(composerJsonPath, 'utf8'));
|
|
28
|
-
const require = parsed.
|
|
28
|
+
const require = requireToInstall(parsed, isLocalEnvironment(this.siteConfig.name));
|
|
29
29
|
const packageCount = Object.keys(require).length;
|
|
30
30
|
const intent = toIntent(require);
|
|
31
31
|
const hasLock = existsSync(composerLockPath);
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
type FormPullResult = {
|
|
3
|
+
orphans: string[];
|
|
4
|
+
path: string;
|
|
5
|
+
skipped: number;
|
|
6
|
+
status: 'dry-run' | 'success';
|
|
7
|
+
};
|
|
2
8
|
export default class Pull extends LoopressCommand {
|
|
3
9
|
static args: {
|
|
4
10
|
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
11
|
};
|
|
6
12
|
static description: string;
|
|
13
|
+
static enableJsonFlag: boolean;
|
|
7
14
|
static examples: string[];
|
|
8
15
|
static flags: {
|
|
9
16
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
17
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
18
|
};
|
|
12
|
-
run(): Promise<
|
|
19
|
+
run(): Promise<FormPullResult>;
|
|
13
20
|
}
|
|
21
|
+
export {};
|
|
@@ -11,6 +11,7 @@ export default class Pull extends LoopressCommand {
|
|
|
11
11
|
path: Args.string({ description: 'Path to forms directory (overrides project config)' }),
|
|
12
12
|
};
|
|
13
13
|
static description = 'Pull forms from WordPress';
|
|
14
|
+
static enableJsonFlag = true;
|
|
14
15
|
static examples = ['$ lps form pull'];
|
|
15
16
|
static flags = {
|
|
16
17
|
...LoopressCommand.dryRunFlag,
|
|
@@ -46,9 +47,10 @@ export default class Pull extends LoopressCommand {
|
|
|
46
47
|
},
|
|
47
48
|
});
|
|
48
49
|
if (this.dryRun)
|
|
49
|
-
return;
|
|
50
|
+
return { orphans, path, skipped, status: 'dry-run' };
|
|
50
51
|
if (skipped > 0) {
|
|
51
52
|
this.warn(`${pluralize(skipped, 'form')} skipped because they have no id`);
|
|
52
53
|
}
|
|
54
|
+
return { orphans, path, skipped, status: 'success' };
|
|
53
55
|
}
|
|
54
56
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { PushCommand } from '../../lib/push-command.js';
|
|
2
|
+
type FormPushResult = {
|
|
3
|
+
path: string;
|
|
4
|
+
status: 'dry-run' | 'success';
|
|
5
|
+
};
|
|
2
6
|
export default class Push extends PushCommand {
|
|
3
7
|
static args: {
|
|
4
8
|
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
9
|
};
|
|
6
10
|
static description: string;
|
|
11
|
+
static enableJsonFlag: boolean;
|
|
7
12
|
static examples: string[];
|
|
8
13
|
static flags: {
|
|
9
14
|
'allow-notifications': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -11,9 +16,10 @@ export default class Push extends PushCommand {
|
|
|
11
16
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
17
|
};
|
|
13
18
|
private allowNotifications;
|
|
14
|
-
run(): Promise<
|
|
19
|
+
run(): Promise<FormPushResult>;
|
|
15
20
|
private currentRevision;
|
|
16
21
|
private ensureCanonicalFilename;
|
|
17
22
|
private loadFiles;
|
|
18
23
|
private pushForm;
|
|
19
24
|
}
|
|
25
|
+
export {};
|
|
@@ -14,6 +14,7 @@ export default class Push extends PushCommand {
|
|
|
14
14
|
path: Args.string({ description: 'Path to forms directory (overrides project config)' }),
|
|
15
15
|
};
|
|
16
16
|
static description = 'Push forms to WordPress. Local files created or updated remotely are renamed on disk to the `<id>-<slug>.json` convention.';
|
|
17
|
+
static enableJsonFlag = true;
|
|
17
18
|
static examples = ['$ lps form push', '$ lps form push --allow-notifications'];
|
|
18
19
|
static flags = {
|
|
19
20
|
...PushCommand.dryRunFlag,
|
|
@@ -41,9 +42,10 @@ export default class Push extends PushCommand {
|
|
|
41
42
|
this.error(`${pluralize(this.failedCount, 'form')} failed to push.`);
|
|
42
43
|
}
|
|
43
44
|
if (this.dryRun)
|
|
44
|
-
return;
|
|
45
|
+
return { path, status: 'dry-run' };
|
|
45
46
|
await this.recordSuccess();
|
|
46
47
|
this.log('All forms pushed.');
|
|
48
|
+
return { path, status: 'success' };
|
|
47
49
|
}
|
|
48
50
|
async currentRevision(id) {
|
|
49
51
|
try {
|
package/dist/commands/init.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { confirm, input, select } from '@inquirer/prompts';
|
|
2
2
|
import { Command } from '@oclif/core';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
@@ -11,19 +11,6 @@ const SNIPPET_PROVIDERS = [
|
|
|
11
11
|
{ name: 'Code Snippets', slug: 'code-snippets' },
|
|
12
12
|
{ name: 'WPCode', slug: 'insert-headers-and-footers' },
|
|
13
13
|
];
|
|
14
|
-
// Default directory per optional feature, matching the resolve*Path defaults in lib/base.ts.
|
|
15
|
-
const FEATURES = [
|
|
16
|
-
{ dir: 'acf', key: 'acfDir', label: 'ACF' },
|
|
17
|
-
{ dir: 'seo', key: 'seoDir', label: 'SEO' },
|
|
18
|
-
{ dir: 'menus', key: 'menuDir', label: 'Menus' },
|
|
19
|
-
{ dir: 'forms', key: 'formDir', label: 'Forms' },
|
|
20
|
-
{ dir: 'api', key: 'apiDir', label: 'Custom API routes' },
|
|
21
|
-
{ dir: 'hooks', key: 'hooksDir', label: 'Hooks (actions, filters, cron)' },
|
|
22
|
-
{ dir: 'apps', key: 'appsDir', label: 'Single-page apps' },
|
|
23
|
-
{ dir: 'pages', key: 'pageDir', label: 'Static pages' },
|
|
24
|
-
{ dir: 'templates', key: 'templateDir', label: 'Block templates' },
|
|
25
|
-
{ dir: 'parts', key: 'partDir', label: 'Block template parts' },
|
|
26
|
-
];
|
|
27
14
|
export default class Init extends Command {
|
|
28
15
|
static description = 'Initialize a loopress.json config file in the current directory';
|
|
29
16
|
static examples = ['$ lps init'];
|
|
@@ -48,27 +35,13 @@ export default class Init extends Command {
|
|
|
48
35
|
default: '.',
|
|
49
36
|
message: 'Root directory',
|
|
50
37
|
});
|
|
51
|
-
const snippetsDir = await input({
|
|
52
|
-
default: 'snippets',
|
|
53
|
-
message: 'Snippets directory (relative to root)',
|
|
54
|
-
});
|
|
55
|
-
const features = await checkbox({
|
|
56
|
-
choices: FEATURES.map((feature) => ({ name: `${feature.label} (${feature.dir}/)`, value: feature.key })),
|
|
57
|
-
message: 'Other features to configure directories for (optional)',
|
|
58
|
-
});
|
|
59
38
|
const providerChoice = await select({
|
|
60
39
|
choices: [...SNIPPET_PROVIDERS.map((p) => ({ name: p.name, value: p.slug })), { name: 'None / already installed', value: '__none__' }],
|
|
61
40
|
message: 'Snippet provider',
|
|
62
41
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
snippetsDir,
|
|
67
|
-
};
|
|
68
|
-
for (const feature of FEATURES) {
|
|
69
|
-
if (features.includes(feature.key))
|
|
70
|
-
config[feature.key] = feature.dir;
|
|
71
|
-
}
|
|
42
|
+
// Resource directories are left out on purpose: every command falls back to its default
|
|
43
|
+
// (utils/resource-dirs.ts), and a custom name is a one-line edit in loopress.json.
|
|
44
|
+
const config = { projectId, rootDir };
|
|
72
45
|
await writeLocalConfig(config);
|
|
73
46
|
const isGitattributesUpdated = await ensureLfGitattributes();
|
|
74
47
|
let isProviderAdded = false;
|
|
@@ -83,19 +56,14 @@ export default class Init extends Command {
|
|
|
83
56
|
}
|
|
84
57
|
this.log(`\n✓ loopress.json created`);
|
|
85
58
|
this.log(` Project: ${projectLabel}`);
|
|
86
|
-
this.log(`
|
|
87
|
-
for (const feature of FEATURES) {
|
|
88
|
-
if (features.includes(feature.key)) {
|
|
89
|
-
this.log(` ${feature.label}:${' '.repeat(Math.max(1, 9 - feature.label.length))}${join(rootDir, feature.dir)}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
59
|
+
this.log(` Root: ${rootDir}`);
|
|
92
60
|
if (isProviderAdded) {
|
|
93
61
|
this.log(` Plugin: ${providerChoice}`);
|
|
94
62
|
}
|
|
95
63
|
if (isGitattributesUpdated) {
|
|
96
64
|
this.log('✓ .gitattributes: LF line endings, so files match across macOS, Linux and Windows');
|
|
97
65
|
}
|
|
98
|
-
this.log('\n→ Next: run `lps
|
|
66
|
+
this.log('\n→ Next: run `lps pull` to fetch what is already on the site, or `lps doctor` to verify the connection.');
|
|
99
67
|
}
|
|
100
68
|
// When nothing is configured yet, the useful path is configuring a project right here, not
|
|
101
69
|
// typing a project ID that points at nothing. Manual entry stays available as an explicit
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
type MenuPullResult = {
|
|
3
|
+
path: string;
|
|
4
|
+
status: 'dry-run' | 'success';
|
|
5
|
+
};
|
|
2
6
|
export default class Pull extends LoopressCommand {
|
|
3
7
|
static args: {
|
|
4
8
|
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
9
|
};
|
|
6
10
|
static description: string;
|
|
11
|
+
static enableJsonFlag: boolean;
|
|
7
12
|
static examples: string[];
|
|
8
13
|
static flags: {
|
|
9
14
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
15
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
16
|
};
|
|
12
|
-
run(): Promise<
|
|
17
|
+
run(): Promise<MenuPullResult>;
|
|
13
18
|
private pullLocations;
|
|
14
19
|
private pullMenus;
|
|
15
20
|
}
|
|
21
|
+
export {};
|
|
@@ -12,6 +12,7 @@ export default class Pull extends LoopressCommand {
|
|
|
12
12
|
path: Args.string({ description: 'Path to menus directory (overrides project config)' }),
|
|
13
13
|
};
|
|
14
14
|
static description = 'Pull nav menus and the active theme menu locations from WordPress';
|
|
15
|
+
static enableJsonFlag = true;
|
|
15
16
|
static examples = ['$ lps menu pull'];
|
|
16
17
|
static flags = {
|
|
17
18
|
...LoopressCommand.dryRunFlag,
|
|
@@ -25,6 +26,7 @@ export default class Pull extends LoopressCommand {
|
|
|
25
26
|
this.log(`Menus path: ${path}`);
|
|
26
27
|
await this.pullMenus(path);
|
|
27
28
|
await this.pullLocations(path);
|
|
29
|
+
return { path, status: this.dryRun ? 'dry-run' : 'success' };
|
|
28
30
|
}
|
|
29
31
|
async pullLocations(basePath) {
|
|
30
32
|
const file = join(basePath, `${LOCATIONS_FILE_BASENAME}.json`);
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { PushCommand } from '../../lib/push-command.js';
|
|
2
|
+
type MenuPushResult = {
|
|
3
|
+
path: string;
|
|
4
|
+
status: 'dry-run' | 'success';
|
|
5
|
+
};
|
|
2
6
|
export default class Push extends PushCommand {
|
|
3
7
|
static args: {
|
|
4
8
|
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
9
|
};
|
|
6
10
|
static description: string;
|
|
11
|
+
static enableJsonFlag: boolean;
|
|
7
12
|
static examples: string[];
|
|
8
13
|
static flags: {
|
|
9
14
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
15
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
16
|
};
|
|
12
|
-
run(): Promise<
|
|
17
|
+
run(): Promise<MenuPushResult>;
|
|
13
18
|
private currentRevision;
|
|
14
19
|
private pushLocations;
|
|
15
20
|
private pushMenuFile;
|
|
16
21
|
private pushMenus;
|
|
17
22
|
}
|
|
23
|
+
export {};
|
|
@@ -15,6 +15,7 @@ export default class Push extends PushCommand {
|
|
|
15
15
|
path: Args.string({ description: 'Path to menus directory (overrides project config)' }),
|
|
16
16
|
};
|
|
17
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 enableJsonFlag = true;
|
|
18
19
|
static examples = ['$ lps menu push'];
|
|
19
20
|
static flags = {
|
|
20
21
|
...PushCommand.dryRunFlag,
|
|
@@ -35,9 +36,10 @@ export default class Push extends PushCommand {
|
|
|
35
36
|
this.error(`${pluralize(this.failedCount, 'menu')} failed to push.`);
|
|
36
37
|
}
|
|
37
38
|
if (this.dryRun)
|
|
38
|
-
return;
|
|
39
|
+
return { path, status: 'dry-run' };
|
|
39
40
|
await this.recordSuccess();
|
|
40
41
|
this.log('All nav menus pushed.');
|
|
42
|
+
return { path, status: 'success' };
|
|
41
43
|
}
|
|
42
44
|
async currentRevision(slug) {
|
|
43
45
|
try {
|
|
@@ -56,19 +56,19 @@ export default class Pull extends LoopressCommand {
|
|
|
56
56
|
}
|
|
57
57
|
// Returns the refreshed option (and writes it to disk), or null when the tracked name is a
|
|
58
58
|
// 404 on this environment: absent, not a failure (see the orphan reconciliation in run()).
|
|
59
|
-
// readonly
|
|
60
|
-
// existing file rather than overwritten by the pulled value.
|
|
59
|
+
// readonly and refs are local declarations with no WordPress counterpart, always preserved
|
|
60
|
+
// from the existing file rather than overwritten by the pulled value.
|
|
61
61
|
async pullOption(dir, option) {
|
|
62
62
|
let remote;
|
|
63
63
|
try {
|
|
64
|
-
remote = await this.wp.get(optionEndpoint(option.name));
|
|
64
|
+
remote = await this.wp.get(optionEndpoint(option.name, option.refs));
|
|
65
65
|
}
|
|
66
66
|
catch (error) {
|
|
67
67
|
if (isNotFoundError(error))
|
|
68
68
|
return null;
|
|
69
69
|
throw error;
|
|
70
70
|
}
|
|
71
|
-
const local = { ...remote, readonly: option.readonly };
|
|
71
|
+
const local = { ...remote, readonly: option.readonly, refs: option.refs };
|
|
72
72
|
await writeFile(join(dir, optionFileName(option.name)), JSON.stringify(local, null, 2) + '\n');
|
|
73
73
|
return local;
|
|
74
74
|
}
|
|
@@ -78,6 +78,8 @@ export default class Push extends PushCommand {
|
|
|
78
78
|
// create): falls back to today's unconditional write, same as before this existed.
|
|
79
79
|
const expectedRevision = await this.currentRevision(option.name);
|
|
80
80
|
const body = { autoload: option.autoload, value: option.value };
|
|
81
|
+
if (option.refs)
|
|
82
|
+
body.refs = option.refs;
|
|
81
83
|
if (expectedRevision !== undefined)
|
|
82
84
|
body.expectedRevision = expectedRevision;
|
|
83
85
|
await this.wp.put(optionEndpoint(option.name), body);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { PushCommand } from '../../lib/push-command.js';
|
|
3
|
+
import { getResourceStateProvider } from '../../lib/resource-state.js';
|
|
3
4
|
import { formatPageProblems, frontPageNote, PAGES_ENDPOINT, readLocalPages } from '../../utils/page-format.js';
|
|
4
5
|
import { pluralize } from '../../utils/pluralize.js';
|
|
5
6
|
import { resolveResourceDir } from '../../utils/resource-dirs.js';
|
|
@@ -28,6 +29,8 @@ export default class Push extends PushCommand {
|
|
|
28
29
|
this.error(`No page "${args.slug}" in ${path} (expected ${args.slug}.html).`);
|
|
29
30
|
}
|
|
30
31
|
this.log(`Found ${pluralize(selected.length, 'page')} to push`);
|
|
32
|
+
if (this.dryRun)
|
|
33
|
+
await this.previewRemote(getResourceStateProvider('page'), path);
|
|
31
34
|
const pushed = [];
|
|
32
35
|
await this.runPushTasks(selected, (page) => page.slug, async (page, task) => {
|
|
33
36
|
await this.pushPage(page, task);
|
|
@@ -4,14 +4,18 @@ import { type MergeResult } from '../../utils/plugins.js';
|
|
|
4
4
|
type PullResult = MergeResult & {
|
|
5
5
|
skipped?: ComposerPullResult['skipped'];
|
|
6
6
|
status: 'dry-run' | 'success';
|
|
7
|
+
untracked: string[];
|
|
7
8
|
};
|
|
8
9
|
export default class Pull extends LoopressCommand {
|
|
9
10
|
static description: string;
|
|
10
11
|
static enableJsonFlag: boolean;
|
|
11
12
|
static examples: string[];
|
|
12
13
|
static flags: {
|
|
14
|
+
plugin: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
16
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
17
|
};
|
|
15
18
|
run(): Promise<PullResult>;
|
|
19
|
+
private selectPlugins;
|
|
16
20
|
}
|
|
17
21
|
export {};
|
|
@@ -1,36 +1,60 @@
|
|
|
1
|
+
import { checkbox } from '@inquirer/prompts';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
1
3
|
import { existsSync } from 'node:fs';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
2
5
|
import { join } from 'node:path';
|
|
3
6
|
import { LoopressCommand } from '../../lib/base.js';
|
|
4
|
-
import {
|
|
7
|
+
import { isInteractive } from '../../lib/interactive.js';
|
|
8
|
+
import { getComposerManagedSlugs, NOT_ON_WORDPRESS_ORG, pullIntoComposerJson, } from '../../utils/composer.js';
|
|
5
9
|
import { writeLocalConfig } from '../../utils/loopress-config.js';
|
|
6
10
|
import { mergePluginManifest, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
7
11
|
export default class Pull extends LoopressCommand {
|
|
8
|
-
static description = 'Pull installed plugins from WordPress into loopress.json, pinned to their live versions'
|
|
12
|
+
static description = 'Pull installed plugins from WordPress into loopress.json (or composer.json), pinned to their live versions. ' +
|
|
13
|
+
'Plugins already tracked are refreshed; new ones are offered as a list to tick, so plugins that only belong ' +
|
|
14
|
+
'on this environment (cache, security, backups) stay out of the project.';
|
|
9
15
|
static enableJsonFlag = true;
|
|
10
|
-
static examples = ['$ lps plugin pull', '$ lps plugin pull --
|
|
16
|
+
static examples = ['$ lps plugin pull', '$ lps plugin pull --env production --plugin query-monitor'];
|
|
11
17
|
static flags = {
|
|
12
18
|
...LoopressCommand.dryRunFlag,
|
|
19
|
+
...LoopressCommand.yesFlag,
|
|
20
|
+
plugin: Flags.string({
|
|
21
|
+
description: 'Start tracking this installed plugin too, without asking (repeatable)',
|
|
22
|
+
multiple: true,
|
|
23
|
+
}),
|
|
13
24
|
};
|
|
14
25
|
async run() {
|
|
26
|
+
const { flags } = await this.parse(Pull);
|
|
15
27
|
const { url } = this.siteConfig;
|
|
16
28
|
this.log(`Pulling plugins from ${url}`);
|
|
17
29
|
const raw = await this.wp.get('wp/v2/plugins');
|
|
18
|
-
const
|
|
30
|
+
const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
|
|
31
|
+
const composerJson = existsSync(composerJsonPath)
|
|
32
|
+
? JSON.parse(await readFile(composerJsonPath, 'utf8'))
|
|
33
|
+
: undefined;
|
|
34
|
+
const declared = new Set(composerJson ? getComposerManagedSlugs(composerJson) : Object.keys(this.localConfig.plugins ?? {}));
|
|
35
|
+
const all = parseInstalledPlugins(raw);
|
|
36
|
+
const selected = await this.selectPlugins(all, declared, flags.plugin ?? [], flags.yes);
|
|
37
|
+
const installed = all.filter((p) => selected.has(p.slug));
|
|
38
|
+
const untracked = all.filter((p) => !selected.has(p.slug)).map((p) => p.slug);
|
|
39
|
+
if (untracked.length > 0) {
|
|
40
|
+
this.log(` - Not tracked: ${untracked.join(', ')} (add one with \`lps plugin pull --plugin <slug>\`)`);
|
|
41
|
+
}
|
|
19
42
|
// Pin every plugin to the version actually running on the site, and record the inactive ones
|
|
20
43
|
// as such so a later `plugin push` doesn't switch them on. A later `plugin push` installs
|
|
21
44
|
// exactly this set via Composer + WPackagist; drift only surfaces when the pinned version and
|
|
22
45
|
// the live version disagree.
|
|
23
46
|
const versions = Object.fromEntries(installed.map((p) => [p.slug, p.version]));
|
|
24
|
-
const pins = Object.fromEntries(installed.map((p) => [p.slug, p.active ? p.version : { active: false, version: p.version }]));
|
|
25
47
|
// A composer.json is authoritative over loopress.json: pin the live versions there instead.
|
|
26
|
-
|
|
27
|
-
if (existsSync(composerJsonPath)) {
|
|
48
|
+
if (composerJson) {
|
|
28
49
|
const result = await pullIntoComposerJson(composerJsonPath, 'plugin', versions, {
|
|
29
50
|
dryRun: this.dryRun,
|
|
30
51
|
log: this.log.bind(this),
|
|
31
52
|
});
|
|
32
|
-
|
|
53
|
+
// Ticked, but Composer can't install it (premium or custom): still not tracked by the project.
|
|
54
|
+
const notInstallable = result.skipped.filter((s) => s.reason === NOT_ON_WORDPRESS_ORG).map((s) => s.slug);
|
|
55
|
+
return { ...result, status: this.dryRun ? 'dry-run' : 'success', untracked: [...untracked, ...notInstallable] };
|
|
33
56
|
}
|
|
57
|
+
const pins = Object.fromEntries(installed.map((p) => [p.slug, p.active ? p.version : { active: false, version: p.version }]));
|
|
34
58
|
const { added, merged, updated } = mergePluginManifest(this.localConfig.plugins ?? {}, pins);
|
|
35
59
|
if (this.dryRun) {
|
|
36
60
|
this.log(`[dry-run] Would write ${Object.keys(merged).length} plugins to loopress.json`);
|
|
@@ -38,7 +62,7 @@ export default class Pull extends LoopressCommand {
|
|
|
38
62
|
this.log(` + ${added.join(', ')}`);
|
|
39
63
|
for (const u of updated)
|
|
40
64
|
this.log(` ~ ${u.slug} (${u.from} → ${u.to})`);
|
|
41
|
-
return { added, merged, status: 'dry-run', updated };
|
|
65
|
+
return { added, merged, status: 'dry-run', untracked, updated };
|
|
42
66
|
}
|
|
43
67
|
await writeLocalConfig({ ...this.localConfig, plugins: merged });
|
|
44
68
|
this.log(`Wrote ${Object.keys(merged).length} plugins to loopress.json`);
|
|
@@ -46,6 +70,28 @@ export default class Pull extends LoopressCommand {
|
|
|
46
70
|
this.log(` + Added: ${added.join(', ')}`);
|
|
47
71
|
for (const u of updated)
|
|
48
72
|
this.log(` ~ Updated: ${u.slug} ${u.from} → ${u.to}`);
|
|
49
|
-
return { added, merged, status: 'success', updated };
|
|
73
|
+
return { added, merged, status: 'success', untracked, updated };
|
|
74
|
+
}
|
|
75
|
+
// Plugins already tracked are always refreshed, plus any --plugin. Every other installed plugin
|
|
76
|
+
// is offered unticked, so a second pull from production never brings Wordfence back on its
|
|
77
|
+
// own. With nobody to ask (--yes, --json, CI, the MCP server), only those two sets count:
|
|
78
|
+
// "track everything" would bring the same problem back silently.
|
|
79
|
+
async selectPlugins(installed, declared, requested, yes) {
|
|
80
|
+
const slugs = new Set(installed.map((p) => p.slug));
|
|
81
|
+
for (const slug of requested) {
|
|
82
|
+
if (!slugs.has(slug))
|
|
83
|
+
this.warn(`--plugin ${slug}: not installed on this environment, skipped.`);
|
|
84
|
+
}
|
|
85
|
+
const selected = new Set([...slugs].filter((slug) => declared.has(slug) || requested.includes(slug)));
|
|
86
|
+
const candidates = installed.filter((p) => !selected.has(p.slug));
|
|
87
|
+
if (yes || candidates.length === 0 || this.jsonEnabled() || !isInteractive())
|
|
88
|
+
return selected;
|
|
89
|
+
const picked = await checkbox({
|
|
90
|
+
choices: candidates.map((p) => ({ name: `${p.slug} ${p.version}${p.active ? '' : ' (inactive)'}`, value: p.slug })),
|
|
91
|
+
message: 'Which of these plugins should the project track? Leave the ones that only belong on this environment unticked.',
|
|
92
|
+
});
|
|
93
|
+
for (const slug of picked)
|
|
94
|
+
selected.add(slug);
|
|
95
|
+
return selected;
|
|
50
96
|
}
|
|
51
97
|
}
|
|
@@ -4,9 +4,9 @@ import { readFile } from 'node:fs/promises';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { confirmUninstall } from '../../lib/interactive.js';
|
|
6
6
|
import { PushCommand } from '../../lib/push-command.js';
|
|
7
|
-
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
7
|
+
import { isLocalEnvironment, isNotFoundError } from '../../lib/wp-client.js';
|
|
8
8
|
import { pinActive, pinVersion, pinVersions, } from '../../types/plugin.js';
|
|
9
|
-
import { toIntent, wpackagistRequire } from '../../utils/composer.js';
|
|
9
|
+
import { requireToInstall, toIntent, wpackagistRequire } from '../../utils/composer.js';
|
|
10
10
|
import { isDowngrade, parseCollisions, SYNC_TIMEOUT_MS, } from '../../utils/plugin-sync.js';
|
|
11
11
|
import { diffPlugins, lockedWpackagistSlugs, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
12
12
|
import { isExactVersion } from '../../utils/version.js';
|
|
@@ -178,11 +178,12 @@ export default class Push extends PushCommand {
|
|
|
178
178
|
const composerJsonPath = join(process.cwd(), this.rootDir, 'composer.json');
|
|
179
179
|
if (existsSync(composerJsonPath)) {
|
|
180
180
|
const composerJson = JSON.parse(await readFile(composerJsonPath, 'utf8'));
|
|
181
|
-
const
|
|
181
|
+
const includeDev = isLocalEnvironment(this.siteConfig.name);
|
|
182
|
+
const manifest = wpackagistRequire(composerJson, 'plugin', includeDev);
|
|
182
183
|
if (Object.keys(manifest).length === 0) {
|
|
183
184
|
this.error('No wpackagist-plugin/* package in composer.json. Run `lps plugin pull` first, or `lps composer push` to push it as is.');
|
|
184
185
|
}
|
|
185
|
-
const require = composerJson
|
|
186
|
+
const require = requireToInstall(composerJson, includeDev);
|
|
186
187
|
return { file: 'composer.json', intent: toIntent(require), manifest, packageCount: Object.keys(require).length };
|
|
187
188
|
}
|
|
188
189
|
if (activate) {
|