@loopress/cli 0.23.0 → 0.24.1
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 +603 -94
- 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/api/rm.d.ts +2 -0
- package/dist/commands/api/rm.js +2 -0
- package/dist/commands/composer/pull.js +6 -16
- package/dist/commands/composer/push.d.ts +3 -0
- package/dist/commands/composer/push.js +24 -7
- 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/form/push.d.ts +2 -0
- package/dist/commands/form/push.js +11 -4
- 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/hook/rm.d.ts +2 -0
- package/dist/commands/hook/rm.js +2 -0
- package/dist/commands/init.js +1 -1
- package/dist/commands/login.js +3 -2
- 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/seo/push.d.ts +2 -0
- package/dist/commands/seo/push.js +16 -4
- 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/hooks/finally.js +4 -1
- 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/local-callback-server.d.ts +14 -1
- package/dist/lib/local-callback-server.js +40 -1
- package/dist/lib/php-files-command.d.ts +67 -0
- package/dist/lib/php-files-command.js +338 -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/sentry.d.ts +3 -0
- package/dist/lib/sentry.js +30 -0
- package/dist/lib/validate-local.js +15 -7
- package/dist/lib/wp-authorize-flow.d.ts +4 -0
- package/dist/lib/wp-authorize-flow.js +10 -8
- 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 +69 -0
- package/dist/utils/plugin-sync.d.ts +6 -0
- package/dist/utils/resource-dirs.d.ts +13 -0
- package/dist/utils/resource-dirs.js +34 -0
- package/oclif.manifest.json +1488 -301
- package/package.json +11 -8
- 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';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { PushCommand } from '../../lib/push-command.js';
|
|
2
|
+
import { type LockDriftEntry } from '../../utils/plugin-sync.js';
|
|
2
3
|
type PushResult = {
|
|
3
4
|
hasLock: boolean;
|
|
5
|
+
lockDrift: LockDriftEntry[];
|
|
4
6
|
packageCount: number;
|
|
5
7
|
status: 'dry-run' | 'success';
|
|
6
8
|
};
|
|
@@ -14,5 +16,6 @@ export default class ComposerPush extends PushCommand {
|
|
|
14
16
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
17
|
};
|
|
16
18
|
run(): Promise<PushResult>;
|
|
19
|
+
private reportLockDrift;
|
|
17
20
|
}
|
|
18
21
|
export {};
|
|
@@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { PushCommand } from '../../lib/push-command.js';
|
|
6
6
|
import { isTimeoutError } from '../../lib/wp-client.js';
|
|
7
|
-
import { parseCollisions, SYNC_TIMEOUT_MS } from '../../utils/plugin-sync.js';
|
|
7
|
+
import { parseCollisions, SYNC_TIMEOUT_MS, } from '../../utils/plugin-sync.js';
|
|
8
8
|
// Split a composer.json `require` map into the three intent namespaces the sync endpoint
|
|
9
9
|
// understands. `composer/installers` is owned by the server scaffold, never sent.
|
|
10
10
|
function toIntent(require) {
|
|
@@ -25,7 +25,7 @@ function toIntent(require) {
|
|
|
25
25
|
return intent;
|
|
26
26
|
}
|
|
27
27
|
export default class ComposerPush extends PushCommand {
|
|
28
|
-
static description = 'Push composer.json
|
|
28
|
+
static description = 'Push composer.json to WordPress and run Composer to resolve and install dependencies';
|
|
29
29
|
static enableJsonFlag = true;
|
|
30
30
|
static examples = ['$ lps composer push', '$ lps composer push --dry-run'];
|
|
31
31
|
static flags = {
|
|
@@ -47,14 +47,16 @@ export default class ComposerPush extends PushCommand {
|
|
|
47
47
|
const packageCount = Object.keys(require).length;
|
|
48
48
|
const intent = toIntent(require);
|
|
49
49
|
const hasLock = existsSync(composerLockPath);
|
|
50
|
+
// The server resolves composer.json itself; it never installs from this lock (a crafted
|
|
51
|
+
// lock could point package downloads at arbitrary hosts). It is sent only so the server
|
|
52
|
+
// can report which pinned versions its own resolution moved. `lps composer pull` brings
|
|
53
|
+
// the resolved lock back.
|
|
50
54
|
const lock = hasLock ? await readFile(composerLockPath, 'utf8') : null;
|
|
51
55
|
this.log(`Pushing composer.json (${packageCount} ${packageCount === 1 ? 'package' : 'packages'}) to ${url}`);
|
|
52
56
|
if (lock)
|
|
53
|
-
this.log(' + composer.lock
|
|
54
|
-
else
|
|
55
|
-
this.warn('No composer.lock found. The server will resolve versions freely.');
|
|
57
|
+
this.log(' + composer.lock sent for drift comparison (the server resolves versions from composer.json)');
|
|
56
58
|
if (this.dryRun)
|
|
57
|
-
return { hasLock, packageCount, status: 'dry-run' };
|
|
59
|
+
return { hasLock, lockDrift: [], packageCount, status: 'dry-run' };
|
|
58
60
|
this.log('Running Composer on the server, this can take a few minutes...');
|
|
59
61
|
let response;
|
|
60
62
|
try {
|
|
@@ -72,7 +74,22 @@ export default class ComposerPush extends PushCommand {
|
|
|
72
74
|
if (response.output.trim())
|
|
73
75
|
this.log(response.output.trim());
|
|
74
76
|
this.log('Composer run completed on the server.');
|
|
77
|
+
const lockDrift = response.lockDrift ?? [];
|
|
78
|
+
this.reportLockDrift(lockDrift);
|
|
75
79
|
await this.recordSuccess();
|
|
76
|
-
return { hasLock, packageCount, status: 'success' };
|
|
80
|
+
return { hasLock, lockDrift, packageCount, status: 'success' };
|
|
81
|
+
}
|
|
82
|
+
// The server resolves composer.json from scratch and never installs from the uploaded lock,
|
|
83
|
+
// so a project that committed a lock can see versions move. Spell out exactly what changed
|
|
84
|
+
// and point at `lps composer pull` to bring the resolved lock back locally.
|
|
85
|
+
reportLockDrift(lockDrift) {
|
|
86
|
+
if (lockDrift.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
this.log('');
|
|
89
|
+
this.log(`The server resolved ${lockDrift.length} ${lockDrift.length === 1 ? 'package' : 'packages'} to a different version than your local composer.lock:`);
|
|
90
|
+
for (const { from, name, to } of lockDrift) {
|
|
91
|
+
this.log(` ${name}: ${from ?? '(absent)'} -> ${to ?? '(removed)'}`);
|
|
92
|
+
}
|
|
93
|
+
this.log('Run `lps composer pull` to update your local composer.json and composer.lock.');
|
|
77
94
|
}
|
|
78
95
|
}
|
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
|
+
});
|
|
@@ -6,9 +6,11 @@ export default class Push extends PushCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
'allow-notifications': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
};
|
|
13
|
+
private allowNotifications;
|
|
12
14
|
run(): Promise<void>;
|
|
13
15
|
private ensureCanonicalFilename;
|
|
14
16
|
private loadFiles;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Args } from '@oclif/core';
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { readFile, rename } from 'node:fs/promises';
|
|
3
3
|
import { dirname, extname, join } from 'node:path';
|
|
4
4
|
import { PushCommand } from '../../lib/push-command.js';
|
|
@@ -12,15 +12,21 @@ export default class Push extends PushCommand {
|
|
|
12
12
|
path: Args.string({ description: 'Path to forms directory (overrides project config)' }),
|
|
13
13
|
};
|
|
14
14
|
static description = 'Push forms to WordPress. Local files created or updated remotely are renamed on disk to the `<id>-<slug>.json` convention.';
|
|
15
|
-
static examples = ['$ lps form push'];
|
|
15
|
+
static examples = ['$ lps form push', '$ lps form push --allow-notifications'];
|
|
16
16
|
static flags = {
|
|
17
17
|
...PushCommand.dryRunFlag,
|
|
18
18
|
...PushCommand.yesFlag,
|
|
19
|
+
'allow-notifications': Flags.boolean({
|
|
20
|
+
default: false,
|
|
21
|
+
description: "Also push each form's notification and confirmation settings (recipients, sender, messages). Off by default: the server keeps its own so a stray push can't redirect submissions.",
|
|
22
|
+
}),
|
|
19
23
|
};
|
|
24
|
+
allowNotifications = false;
|
|
20
25
|
async run() {
|
|
21
|
-
const { args } = await this.parse(Push);
|
|
26
|
+
const { args, flags } = await this.parse(Push);
|
|
22
27
|
const { url } = this.siteConfig;
|
|
23
28
|
const path = this.resolveFormPath(args.path);
|
|
29
|
+
this.allowNotifications = flags['allow-notifications'];
|
|
24
30
|
this.log(`Pushing forms to ${url}`);
|
|
25
31
|
this.log(`Forms path: ${path}`);
|
|
26
32
|
const files = await this.loadFiles(path);
|
|
@@ -78,9 +84,10 @@ export default class Push extends PushCommand {
|
|
|
78
84
|
}
|
|
79
85
|
try {
|
|
80
86
|
const id = getFormId(data);
|
|
87
|
+
const payload = this.allowNotifications ? { ...data, allowNotifications: true } : data;
|
|
81
88
|
const { body, created } = await putOrCreate(this.wp, {
|
|
82
89
|
id,
|
|
83
|
-
payload
|
|
90
|
+
payload,
|
|
84
91
|
postEndpoint: FORM_ENDPOINT,
|
|
85
92
|
putEndpoint: (formId) => `${FORM_ENDPOINT}/${formId}`,
|
|
86
93
|
});
|
|
@@ -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 {
|
package/dist/commands/login.js
CHANGED
|
@@ -13,8 +13,9 @@ export default class Login extends Command {
|
|
|
13
13
|
}
|
|
14
14
|
async waitForCallback() {
|
|
15
15
|
return waitForLocalCallback({
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
allowedOrigins: [CONSOLE_URL],
|
|
17
|
+
buildUrl(callbackBaseUrl, state) {
|
|
18
|
+
const callbackUrl = encodeURIComponent(`${callbackBaseUrl}/callback?state=${state}`);
|
|
18
19
|
return `${CONSOLE_URL}/cli-auth?callbackUrl=${callbackUrl}`;
|
|
19
20
|
},
|
|
20
21
|
handleRequest(url, { resolveWithPage, respondBadRequest }) {
|
|
@@ -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
|
+
}
|