@loopress/cli 0.23.0 → 0.24.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 +515 -83
- package/dist/commands/acf/diff.d.ts +2 -0
- package/dist/commands/acf/diff.js +5 -0
- package/dist/commands/api/diff.d.ts +2 -0
- package/dist/commands/api/diff.js +5 -0
- package/dist/commands/api/list.d.ts +2 -12
- package/dist/commands/api/list.js +2 -20
- package/dist/commands/api/pull.d.ts +2 -20
- package/dist/commands/api/pull.js +2 -53
- package/dist/commands/api/push.d.ts +2 -22
- package/dist/commands/api/push.js +2 -116
- package/dist/commands/composer/pull.js +6 -16
- package/dist/commands/dev.js +1 -1
- package/dist/commands/diff.d.ts +13 -0
- package/dist/commands/diff.js +42 -0
- package/dist/commands/form/diff.d.ts +2 -0
- package/dist/commands/form/diff.js +5 -0
- package/dist/commands/hook/diff.d.ts +2 -0
- package/dist/commands/hook/diff.js +5 -0
- package/dist/commands/hook/list.d.ts +2 -0
- package/dist/commands/hook/list.js +2 -0
- package/dist/commands/hook/pull.d.ts +2 -0
- package/dist/commands/hook/pull.js +2 -0
- package/dist/commands/hook/push.d.ts +2 -0
- package/dist/commands/hook/push.js +2 -0
- package/dist/commands/init.js +1 -1
- package/dist/commands/option/add.d.ts +15 -0
- package/dist/commands/option/add.js +47 -0
- package/dist/commands/option/diff.d.ts +2 -0
- package/dist/commands/option/diff.js +5 -0
- package/dist/commands/{page → option}/list.d.ts +5 -1
- package/dist/commands/option/list.js +151 -0
- package/dist/commands/{page → option}/pull.d.ts +3 -12
- package/dist/commands/option/pull.js +75 -0
- package/dist/commands/{page → option}/push.d.ts +3 -8
- package/dist/commands/option/push.js +66 -0
- package/dist/commands/option/remove.d.ts +22 -0
- package/dist/commands/option/remove.js +69 -0
- package/dist/commands/plugin/push.d.ts +0 -1
- package/dist/commands/plugin/push.js +3 -10
- package/dist/commands/pull.js +3 -2
- package/dist/commands/push.js +4 -3
- package/dist/commands/seo/diff.d.ts +2 -0
- package/dist/commands/seo/diff.js +5 -0
- package/dist/commands/snippet/diff.d.ts +2 -0
- package/dist/commands/snippet/diff.js +5 -0
- package/dist/commands/theme/push.d.ts +0 -1
- package/dist/commands/theme/push.js +3 -10
- package/dist/lib/base.d.ts +2 -1
- package/dist/lib/base.js +12 -22
- package/dist/lib/dev-watch.d.ts +1 -1
- package/dist/lib/dev-watch.js +1 -2
- package/dist/lib/diff-command.d.ts +47 -0
- package/dist/lib/diff-command.js +180 -0
- package/dist/lib/diff-state.d.ts +22 -0
- package/dist/lib/diff-state.js +88 -0
- package/dist/lib/find-orphaned-files.js +1 -1
- package/dist/lib/interactive.d.ts +1 -0
- package/dist/lib/interactive.js +10 -0
- package/dist/lib/php-files-command.d.ts +54 -0
- package/dist/lib/php-files-command.js +216 -0
- package/dist/lib/put-or-create.js +1 -1
- package/dist/lib/readdir-tolerant.d.ts +1 -0
- package/dist/lib/readdir-tolerant.js +3 -1
- package/dist/lib/resource-state.d.ts +14 -0
- package/dist/lib/resource-state.js +361 -0
- package/dist/lib/validate-local.js +15 -7
- package/dist/lib/wp-client.d.ts +5 -0
- package/dist/lib/wp-client.js +56 -0
- package/dist/types/project-config.generated.d.ts +8 -4
- package/dist/utils/option-format.d.ts +31 -0
- package/dist/utils/option-format.js +51 -0
- package/dist/utils/resource-dirs.d.ts +13 -0
- package/dist/utils/resource-dirs.js +34 -0
- package/oclif.manifest.json +1156 -149
- package/package.json +10 -7
- package/schemas/project-config.schema.json +10 -5
- package/dist/commands/page/list.js +0 -19
- package/dist/commands/page/pull.js +0 -57
- package/dist/commands/page/push.js +0 -132
- package/dist/utils/page-format.d.ts +0 -7
- package/dist/utils/page-format.js +0 -78
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('acf', {
|
|
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
|
+
pathNoun: 'ACF directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('api', {
|
|
3
|
+
description: 'Show what differs in custom API route files between your local files and a WordPress environment, or between two environments',
|
|
4
|
+
pathNoun: 'api directory',
|
|
5
|
+
});
|
|
@@ -1,12 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
content: string;
|
|
4
|
-
filename: string;
|
|
5
|
-
};
|
|
6
|
-
export default class List extends LoopressCommand {
|
|
7
|
-
static description: string;
|
|
8
|
-
static enableJsonFlag: boolean;
|
|
9
|
-
static examples: string[];
|
|
10
|
-
run(): Promise<ApiFile[]>;
|
|
11
|
-
}
|
|
12
|
-
export {};
|
|
1
|
+
declare const _default: import("../../lib/php-files-command.js").CommandClass<import("../../lib/php-files-command.js").ListFilesCommand>;
|
|
2
|
+
export default _default;
|
|
@@ -1,20 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export default class List extends LoopressCommand {
|
|
4
|
-
static description = 'List custom API route files from WordPress';
|
|
5
|
-
static enableJsonFlag = true;
|
|
6
|
-
static examples = ['$ lps api list'];
|
|
7
|
-
async run() {
|
|
8
|
-
const files = await this.wp.get('loopress/v1/api-files');
|
|
9
|
-
if (files.length === 0) {
|
|
10
|
-
this.log('No API route files found');
|
|
11
|
-
return files;
|
|
12
|
-
}
|
|
13
|
-
this.log(`Found ${pluralize(files.length, 'route file')}:`);
|
|
14
|
-
this.log('');
|
|
15
|
-
for (const file of files) {
|
|
16
|
-
this.log(` ${file.filename}`);
|
|
17
|
-
}
|
|
18
|
-
return files;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
1
|
+
import { API_FILES_RESOURCE, resourceListCommand } from '../../lib/php-files-command.js';
|
|
2
|
+
export default resourceListCommand(API_FILES_RESOURCE);
|
|
@@ -1,20 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
orphans: string[];
|
|
4
|
-
pulled: string[];
|
|
5
|
-
status: 'dry-run' | 'success';
|
|
6
|
-
};
|
|
7
|
-
export default class Pull extends LoopressCommand {
|
|
8
|
-
static args: {
|
|
9
|
-
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
10
|
-
};
|
|
11
|
-
static description: string;
|
|
12
|
-
static enableJsonFlag: boolean;
|
|
13
|
-
static examples: string[];
|
|
14
|
-
static flags: {
|
|
15
|
-
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
-
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
-
};
|
|
18
|
-
run(): Promise<PullResult>;
|
|
19
|
-
}
|
|
20
|
-
export {};
|
|
1
|
+
declare const _default: import("../../lib/php-files-command.js").CommandClass<import("../../lib/php-files-command.js").PullFilesCommand>;
|
|
2
|
+
export default _default;
|
|
@@ -1,53 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { LoopressCommand } from '../../lib/base.js';
|
|
5
|
-
import { basenameKey, findOrphanedFiles } from '../../lib/find-orphaned-files.js';
|
|
6
|
-
import { pluralize } from '../../utils/pluralize.js';
|
|
7
|
-
export default class Pull extends LoopressCommand {
|
|
8
|
-
static args = {
|
|
9
|
-
path: Args.string({ description: 'Path to api directory (overrides project config)' }),
|
|
10
|
-
};
|
|
11
|
-
static description = 'Pull custom API route files from WordPress';
|
|
12
|
-
static enableJsonFlag = true;
|
|
13
|
-
static examples = ['$ lps api pull', '$ lps api pull --path ./api'];
|
|
14
|
-
static flags = {
|
|
15
|
-
...LoopressCommand.dryRunFlag,
|
|
16
|
-
...LoopressCommand.yesFlag,
|
|
17
|
-
};
|
|
18
|
-
async run() {
|
|
19
|
-
const { args } = await this.parse(Pull);
|
|
20
|
-
const { url } = this.siteConfig;
|
|
21
|
-
const path = this.resolveApiPath(args.path);
|
|
22
|
-
this.log(`Pulling API routes from ${url}`);
|
|
23
|
-
this.log(`API path: ${path}`);
|
|
24
|
-
const files = await this.wp.get('loopress/v1/api-files');
|
|
25
|
-
// A `<filename>.php` no longer present remotely belongs to a route deleted on WordPress
|
|
26
|
-
// (push itself stays additive-only, but pull already cleans up locally, same as
|
|
27
|
-
// `snippet pull`).
|
|
28
|
-
const orphans = await findOrphanedFiles(path, new Set(files.map((file) => file.filename)), {
|
|
29
|
-
extensions: ['.php'],
|
|
30
|
-
key: basenameKey,
|
|
31
|
-
recursive: true,
|
|
32
|
-
});
|
|
33
|
-
const pulled = files.map((file) => file.filename);
|
|
34
|
-
await this.pullDirectory(path, files, orphans, {
|
|
35
|
-
alwaysCreateDir: true,
|
|
36
|
-
dryRunMessage: `Would pull ${pluralize(files.length, 'route file')} to ${path}`,
|
|
37
|
-
orphanReason: 'whose route no longer exists on WordPress',
|
|
38
|
-
pulledMessage: `Pulled ${pluralize(files.length, 'route file')} to ${path}`,
|
|
39
|
-
title: (file) => file.filename,
|
|
40
|
-
async write(file, writeDir) {
|
|
41
|
-
const filePath = join(writeDir, `${file.filename}.php`);
|
|
42
|
-
// filename can contain '/' (a path-param route, e.g. invoice-pdf/[order_id]):
|
|
43
|
-
// writeFile() doesn't create parent directories on its own the way mkdir()'s
|
|
44
|
-
// recursive option does above for the top-level path.
|
|
45
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
46
|
-
await writeFile(filePath, file.content);
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
if (this.dryRun)
|
|
50
|
-
return { orphans, pulled, status: 'dry-run' };
|
|
51
|
-
return { orphans, pulled, status: 'success' };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
import { API_FILES_RESOURCE, resourcePullCommand } from '../../lib/php-files-command.js';
|
|
2
|
+
export default resourcePullCommand(API_FILES_RESOURCE);
|
|
@@ -1,22 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
pushed: string[];
|
|
4
|
-
status: 'dry-run' | 'success';
|
|
5
|
-
};
|
|
6
|
-
export default class Push extends PushCommand {
|
|
7
|
-
static args: {
|
|
8
|
-
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
9
|
-
};
|
|
10
|
-
static description: string;
|
|
11
|
-
static enableJsonFlag: boolean;
|
|
12
|
-
static examples: string[];
|
|
13
|
-
static flags: {
|
|
14
|
-
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
-
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
-
};
|
|
17
|
-
run(): Promise<PushResult>;
|
|
18
|
-
private loadFiles;
|
|
19
|
-
private pushFile;
|
|
20
|
-
private syncApiRoutes;
|
|
21
|
-
}
|
|
22
|
-
export {};
|
|
1
|
+
declare const _default: import("../../lib/php-files-command.js").CommandClass<import("../../lib/php-files-command.js").PushFilesCommand>;
|
|
2
|
+
export default _default;
|
|
@@ -1,116 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { authManager } from '../../config/auth.manager.js';
|
|
4
|
-
import { ApiClient } from '../../lib/api-client.js';
|
|
5
|
-
import { loadFiles as loadDirectoryFiles } from '../../lib/load-files.js';
|
|
6
|
-
import { PushCommand } from '../../lib/push-command.js';
|
|
7
|
-
import { pluralize } from '../../utils/pluralize.js';
|
|
8
|
-
// Mirrors the server's own allowlist (wordpress-plugin ApiFilesController::isValidFilename()):
|
|
9
|
-
// filename is sent as a body param (not a URL path segment: a nested path can contain '/' and
|
|
10
|
-
// '[]', and depending on a server to handle a percent-encoded slash in a URL path correctly is
|
|
11
|
-
// exactly the kind of hosting-environment variance Loopress can't assume away), but validating
|
|
12
|
-
// client-side first still turns a malformed name into an accurate error and skips a network
|
|
13
|
-
// round-trip that could only ever fail.
|
|
14
|
-
const FILENAME_PATTERN = /^(?:[a-z0-9-]+|\[[A-Za-z_]\w*\])(?:\/(?:[a-z0-9-]+|\[[A-Za-z_]\w*\]))*$/;
|
|
15
|
-
// Mirrors the server's own check (wordpress-plugin FileWriter::DECLARE_PATTERN /
|
|
16
|
-
// withGuard()): the server rejects both an absent declare(strict_types=1); and one that
|
|
17
|
-
// appears more than once (it needs a single unambiguous insertion point for the ABSPATH
|
|
18
|
-
// guard). Matching that "exactly once" rule here too, not just presence, so a file that
|
|
19
|
-
// would still fail server-side doesn't falsely pass this earlier check.
|
|
20
|
-
const DECLARE_PATTERN = /declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;/g;
|
|
21
|
-
export default class Push extends PushCommand {
|
|
22
|
-
static args = {
|
|
23
|
-
path: Args.string({ description: 'Path to api directory (overrides project config)' }),
|
|
24
|
-
};
|
|
25
|
-
static description = 'Push custom API route files to WordPress';
|
|
26
|
-
static enableJsonFlag = true;
|
|
27
|
-
static examples = ['$ lps api push', '$ lps api push --path ./api'];
|
|
28
|
-
static flags = {
|
|
29
|
-
...PushCommand.dryRunFlag,
|
|
30
|
-
...PushCommand.yesFlag,
|
|
31
|
-
};
|
|
32
|
-
async run() {
|
|
33
|
-
const { args } = await this.parse(Push);
|
|
34
|
-
const { url } = this.siteConfig;
|
|
35
|
-
const path = this.resolveApiPath(args.path);
|
|
36
|
-
this.log(`Pushing API routes to ${url}`);
|
|
37
|
-
this.log(`API path: ${path}`);
|
|
38
|
-
const files = await this.loadFiles(path);
|
|
39
|
-
this.log(`Found ${pluralize(files.length, 'route file')} to push`);
|
|
40
|
-
const pushed = [];
|
|
41
|
-
await this.runPushTasks(files, (file) => file.filename, async (file, task) => {
|
|
42
|
-
await this.pushFile(file, task);
|
|
43
|
-
pushed.push(file.filename);
|
|
44
|
-
});
|
|
45
|
-
if (this.failedCount > 0) {
|
|
46
|
-
this.error(`${pluralize(this.failedCount, 'route file')} failed to push.`);
|
|
47
|
-
}
|
|
48
|
-
if (this.dryRun)
|
|
49
|
-
return { pushed, status: 'dry-run' };
|
|
50
|
-
await this.recordSuccess();
|
|
51
|
-
await this.syncApiRoutes(files.map((file) => file.filename));
|
|
52
|
-
this.log('All API routes pushed.');
|
|
53
|
-
return { pushed, status: 'success' };
|
|
54
|
-
}
|
|
55
|
-
async loadFiles(path) {
|
|
56
|
-
return loadDirectoryFiles(path, {
|
|
57
|
-
extension: '.php',
|
|
58
|
-
onSkip: (message) => { this.warn(message); },
|
|
59
|
-
// relative()'s separator is OS-specific ('\\' on Windows); the server only ever expects
|
|
60
|
-
// '/', same as any URL or import path.
|
|
61
|
-
parse: (raw, filePath) => ({
|
|
62
|
-
content: raw,
|
|
63
|
-
filename: relative(path, filePath).slice(0, -'.php'.length).split(sep).join('/'),
|
|
64
|
-
}),
|
|
65
|
-
recursive: true,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
async pushFile(file, task) {
|
|
69
|
-
if (!FILENAME_PATTERN.test(file.filename)) {
|
|
70
|
-
const message = `Invalid filename "${file.filename}": each path segment must be lowercase letters, digits, and hyphens, or a bracketed dynamic segment like "[order_id]" (e.g. "invoice-pdf/[order_id].php")`;
|
|
71
|
-
this.reportTaskFailure(message, new Error(message), task);
|
|
72
|
-
}
|
|
73
|
-
const declareCount = file.content.match(DECLARE_PATTERN)?.length ?? 0;
|
|
74
|
-
if (declareCount !== 1) {
|
|
75
|
-
const reason = declareCount === 0 ? 'is missing' : 'appears more than once';
|
|
76
|
-
const message = `${file.filename}.php: "declare(strict_types=1);" ${reason}, it must appear exactly once as the first statement`;
|
|
77
|
-
this.reportTaskFailure(message, new Error(message), task);
|
|
78
|
-
}
|
|
79
|
-
if (this.dryRun) {
|
|
80
|
-
if (task)
|
|
81
|
-
task.output = `[dry-run] Would push: ${file.filename}`;
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
const result = await this.wp.put('loopress/v1/api-files', {
|
|
86
|
-
content: file.content,
|
|
87
|
-
filename: file.filename,
|
|
88
|
-
});
|
|
89
|
-
if (task) {
|
|
90
|
-
task.output =
|
|
91
|
-
result.syntax_check === 'skipped'
|
|
92
|
-
? `Pushed: ${file.filename} (syntax check skipped, unavailable on this host)`
|
|
93
|
-
: `Pushed: ${file.filename}`;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
catch (error) {
|
|
97
|
-
this.reportTaskFailure(`Failed to push ${file.filename}: ${error.message}`, error, task);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// Best-effort report of the current file list to the Loopress cloud, purely for console
|
|
101
|
-
// visibility (see obsidian/Product/API and Console Backlog.md, US-18): the routes are already
|
|
102
|
-
// live on WordPress by this point, this can never block or fail the push itself, same
|
|
103
|
-
// reasoning as `recordDeployment` in PushCommand.
|
|
104
|
-
async syncApiRoutes(filenames) {
|
|
105
|
-
const token = process.env.LOOPRESS_TOKEN ?? authManager.getAuth()?.token;
|
|
106
|
-
const environmentId = this.siteConfig.apiEnvironmentId;
|
|
107
|
-
if (!token || !environmentId)
|
|
108
|
-
return;
|
|
109
|
-
try {
|
|
110
|
-
await new ApiClient(token).put('api-routes', { environmentId, filenames });
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
// non-blocking: reporting the route list must never interrupt the push flow
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
1
|
+
import { API_FILES_RESOURCE, resourcePushCommand } from '../../lib/php-files-command.js';
|
|
2
|
+
export default resourcePushCommand(API_FILES_RESOURCE);
|
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
import { writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { LoopressCommand } from '../../lib/base.js';
|
|
4
|
-
import {
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
import { isApplicative404 } from '../../lib/wp-client.js';
|
|
5
|
+
// A site that never had Composer dependencies pushed has no composer.lock yet: the Loopress
|
|
6
|
+
// controller answers that specific 404 with `{"error": "composer.lock not found"}`. Any other
|
|
7
|
+
// 404 (route absent because the plugin isn't installed or predates Composer support) must
|
|
8
|
+
// still surface the normal "is the plugin installed?" guidance instead of being swallowed.
|
|
8
9
|
function isMissingComposerLock(error) {
|
|
9
|
-
|
|
10
|
-
return false;
|
|
11
|
-
const body = error.cause?.response?.body;
|
|
12
|
-
if (!body)
|
|
13
|
-
return false;
|
|
14
|
-
try {
|
|
15
|
-
const parsed = JSON.parse(body);
|
|
16
|
-
return parsed.error === 'composer.lock not found';
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
10
|
+
return isApplicative404(error, 'composer.lock not found');
|
|
21
11
|
}
|
|
22
12
|
export default class ComposerPull extends LoopressCommand {
|
|
23
13
|
static description = 'Pull composer.json and composer.lock from WordPress';
|
package/dist/commands/dev.js
CHANGED
|
@@ -9,7 +9,7 @@ const DEBOUNCE_MS = 400;
|
|
|
9
9
|
const IGNORED = /(?:(^|[/\\])(\.git|node_modules|\.DS_Store)(?:[/\\]|$))|(?:\.swp$)/;
|
|
10
10
|
export default class Dev extends Command {
|
|
11
11
|
static description = 'Watch project files and push changes to the local WordPress instance as they happen. Always targets the "local" environment, run `lps snippet push` etc. directly for any other environment.';
|
|
12
|
-
static examples = ['$ lps dev', '$ lps dev --only=snippets,
|
|
12
|
+
static examples = ['$ lps dev', '$ lps dev --only=snippets,api', '$ lps dev --skip=plugins'];
|
|
13
13
|
static flags = {
|
|
14
14
|
only: Flags.string({ description: `Only watch these resource types (comma-separated): ${RESOURCE_TYPES.join(', ')}` }),
|
|
15
15
|
skip: Flags.string({ description: `Skip these resource types (comma-separated): ${RESOURCE_TYPES.join(', ')}` }),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DiffCommand, type DiffJson } from '../lib/diff-command.js';
|
|
2
|
+
export default class Diff extends DiffCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static enableJsonFlag: boolean;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
only: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
skip: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
against: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<DiffJson>;
|
|
12
|
+
private selectResources;
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { DiffCommand } from '../lib/diff-command.js';
|
|
3
|
+
import { RESOURCE_STATE_PROVIDERS } from '../lib/resource-state.js';
|
|
4
|
+
// Everything `lps diff` can compare, in report order. Composer isn't a directory-backed
|
|
5
|
+
// provider (see resource-state.ts) so it's named here explicitly.
|
|
6
|
+
const RESOURCES = [...RESOURCE_STATE_PROVIDERS.map((provider) => provider.resource), 'composer'];
|
|
7
|
+
export default class Diff extends DiffCommand {
|
|
8
|
+
static description = [
|
|
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, SEO, and Composer. Plugins and themes have their own `lps plugin status` / `lps theme status`.',
|
|
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
|
+
].join(' ');
|
|
13
|
+
static enableJsonFlag = true;
|
|
14
|
+
static examples = [
|
|
15
|
+
'$ lps diff',
|
|
16
|
+
'$ lps diff --env staging',
|
|
17
|
+
'$ lps diff --env staging --against production',
|
|
18
|
+
'$ lps diff --only snippet --only acf',
|
|
19
|
+
'$ lps diff --skip composer',
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
...DiffCommand.againstFlag,
|
|
23
|
+
only: Flags.string({ description: 'Only compare these resources', multiple: true, options: [...RESOURCES] }),
|
|
24
|
+
skip: Flags.string({ description: 'Compare every resource except these', multiple: true, options: [...RESOURCES] }),
|
|
25
|
+
};
|
|
26
|
+
async run() {
|
|
27
|
+
const { flags } = await this.parse(Diff);
|
|
28
|
+
const sides = this.resolveSides(flags.against);
|
|
29
|
+
const selected = this.selectResources(flags.only, flags.skip);
|
|
30
|
+
const targets = [
|
|
31
|
+
...RESOURCE_STATE_PROVIDERS.filter((provider) => selected.has(provider.resource)).map((provider) => this.providerTarget(provider, sides)),
|
|
32
|
+
...(selected.has('composer') ? [this.composerTarget(sides)] : []),
|
|
33
|
+
];
|
|
34
|
+
return this.report(targets, sides);
|
|
35
|
+
}
|
|
36
|
+
// `--only` narrows the default set, `--skip` then removes from whatever that set is, same
|
|
37
|
+
// precedence a combined `--only=a,b --skip=b` reads as (mirrors dev-watch's resolveResourceTypes).
|
|
38
|
+
selectResources(only, skip) {
|
|
39
|
+
const base = only && only.length > 0 ? only : RESOURCES;
|
|
40
|
+
return new Set(base.filter((resource) => !(skip ?? []).includes(resource)));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('form', {
|
|
3
|
+
description: 'Show what differs in forms between your local files and a WordPress environment, or between two environments',
|
|
4
|
+
pathNoun: 'forms directory',
|
|
5
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('hook', {
|
|
3
|
+
description: 'Show what differs in hook files between your local files and a WordPress environment, or between two environments',
|
|
4
|
+
pathNoun: 'hooks directory',
|
|
5
|
+
});
|
package/dist/commands/init.js
CHANGED
|
@@ -16,8 +16,8 @@ const FEATURES = [
|
|
|
16
16
|
{ dir: 'acf', key: 'acfDir', label: 'ACF' },
|
|
17
17
|
{ dir: 'seo', key: 'seoDir', label: 'SEO' },
|
|
18
18
|
{ dir: 'forms', key: 'formDir', label: 'Forms' },
|
|
19
|
-
{ dir: 'pages', key: 'pageDir', label: 'Pages' },
|
|
20
19
|
{ dir: 'api', key: 'apiDir', label: 'Custom API routes' },
|
|
20
|
+
{ dir: 'hooks', key: 'hooksDir', label: 'Hooks (actions, filters, cron)' },
|
|
21
21
|
{ dir: 'apps', key: 'appsDir', label: 'Single-page apps' },
|
|
22
22
|
];
|
|
23
23
|
export default class Init extends Command {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { type LocalOption } from '../../utils/option-format.js';
|
|
3
|
+
export default class Add extends LoopressCommand {
|
|
4
|
+
static args: {
|
|
5
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static enableJsonFlag: boolean;
|
|
9
|
+
static examples: string[];
|
|
10
|
+
static flags: {
|
|
11
|
+
path: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
run(): Promise<LocalOption>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { LoopressCommand } from '../../lib/base.js';
|
|
5
|
+
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
6
|
+
import { defaultReadonlyFor, isReservedOptionName, optionEndpoint, optionFileName, } from '../../utils/option-format.js';
|
|
7
|
+
export default class Add extends LoopressCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
name: Args.string({ description: 'Option name to start tracking (see `lps option list`)', required: true }),
|
|
10
|
+
};
|
|
11
|
+
static description = 'Fetch a WordPress option by name and start tracking it locally. Run `lps option list` first to find the name: ' +
|
|
12
|
+
'this copies a raw value across environments, verify it does not embed post/user IDs before pushing it elsewhere.';
|
|
13
|
+
static enableJsonFlag = true;
|
|
14
|
+
static examples = ['$ lps option add wpseo_titles', '$ lps option add siteurl'];
|
|
15
|
+
static flags = {
|
|
16
|
+
...LoopressCommand.dryRunFlag,
|
|
17
|
+
path: Flags.string({ description: 'Path to options directory (overrides project config)' }),
|
|
18
|
+
};
|
|
19
|
+
async run() {
|
|
20
|
+
const { args, flags } = await this.parse(Add);
|
|
21
|
+
const { name } = args;
|
|
22
|
+
const dir = this.resolveOptionsPath(flags.path);
|
|
23
|
+
if (isReservedOptionName(name)) {
|
|
24
|
+
this.error(`"${name}" is managed by \`lps plugin\`/\`lps theme\`, not \`lps option\`.`);
|
|
25
|
+
}
|
|
26
|
+
let remote;
|
|
27
|
+
try {
|
|
28
|
+
remote = await this.wp.get(optionEndpoint(name));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (isNotFoundError(error)) {
|
|
32
|
+
this.error(`Option "${name}" was not found on ${this.siteConfig.url}. Run \`lps option list\` to see available names.`);
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
const local = { ...remote, readonly: defaultReadonlyFor(name) };
|
|
37
|
+
const file = join(dir, optionFileName(name));
|
|
38
|
+
if (this.dryRun) {
|
|
39
|
+
this.log(`[dry-run] Would track "${name}" at ${file}`);
|
|
40
|
+
return local;
|
|
41
|
+
}
|
|
42
|
+
await mkdir(dir, { recursive: true });
|
|
43
|
+
await writeFile(file, JSON.stringify(local, null, 2) + '\n');
|
|
44
|
+
this.log(`Tracking "${name}"${local.readonly ? ' (readonly: push will skip it, see the file to override)' : ''} at ${file}`);
|
|
45
|
+
return local;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { resourceDiffCommand } from '../../lib/diff-command.js';
|
|
2
|
+
export default resourceDiffCommand('option', {
|
|
3
|
+
description: 'Show what differs, for locally tracked options, between your local files and a WordPress environment, or between two environments',
|
|
4
|
+
pathNoun: 'options directory',
|
|
5
|
+
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { LoopressCommand } from '../../lib/base.js';
|
|
2
|
+
import { type ListedOption } from '../../utils/option-format.js';
|
|
2
3
|
export default class List extends LoopressCommand {
|
|
3
4
|
static description: string;
|
|
4
5
|
static enableJsonFlag: boolean;
|
|
5
6
|
static examples: string[];
|
|
6
|
-
|
|
7
|
+
static flags: {
|
|
8
|
+
'no-core': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<ListedOption[]>;
|
|
7
11
|
}
|