@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
package/dist/lib/wp-client.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export declare class WpClient {
|
|
|
9
9
|
constructor(siteUrl: string, token: string);
|
|
10
10
|
delete<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
|
|
11
11
|
get<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
12
|
+
getAll<T>(path: string, options?: RequestOptions & {
|
|
13
|
+
perPage?: number;
|
|
14
|
+
}): Promise<T[]>;
|
|
12
15
|
post<T = unknown>(path: string, json?: Record<string, unknown>, options?: RequestOptions): Promise<T>;
|
|
13
16
|
put<T = unknown>(path: string, json?: Record<string, unknown>, options?: RequestOptions): Promise<T>;
|
|
14
17
|
private request;
|
|
@@ -17,6 +20,8 @@ type RequestOptions = {
|
|
|
17
20
|
timeoutMs?: number;
|
|
18
21
|
};
|
|
19
22
|
export declare function isNotFoundError(error: unknown): boolean;
|
|
23
|
+
export declare function isApplicative404(error: unknown, marker: string): boolean;
|
|
20
24
|
export declare function isTimeoutError(error: unknown): boolean;
|
|
25
|
+
export declare function isInvalidPageNumber(error: unknown): boolean;
|
|
21
26
|
export declare function formatWpError(error: unknown, url: string, timeoutMs?: number): string;
|
|
22
27
|
export {};
|
package/dist/lib/wp-client.js
CHANGED
|
@@ -22,6 +22,32 @@ export class WpClient {
|
|
|
22
22
|
async get(path, options) {
|
|
23
23
|
return this.request('get', path, undefined, options);
|
|
24
24
|
}
|
|
25
|
+
// Fetches every page of a paginated WP REST collection by walking `?page=N`, stopping on the
|
|
26
|
+
// first short page or on WordPress core's 400 "rest_post_invalid_page_number" past the last
|
|
27
|
+
// one. `path` may already carry query params. Use for `wp/v2/*` list endpoints, which cap at
|
|
28
|
+
// 100 items per request.
|
|
29
|
+
async getAll(path, options) {
|
|
30
|
+
const perPage = options?.perPage ?? 100;
|
|
31
|
+
const separator = path.includes('?') ? '&' : '?';
|
|
32
|
+
const all = [];
|
|
33
|
+
for (let page = 1;; page += 1) {
|
|
34
|
+
let batch;
|
|
35
|
+
try {
|
|
36
|
+
batch = await this.get(`${path}${separator}per_page=${perPage}&page=${page}`, options);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (isInvalidPageNumber(error))
|
|
40
|
+
break;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
if (!Array.isArray(batch) || batch.length === 0)
|
|
44
|
+
break;
|
|
45
|
+
all.push(...batch);
|
|
46
|
+
if (batch.length < perPage)
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
return all;
|
|
50
|
+
}
|
|
25
51
|
async post(path, json, options) {
|
|
26
52
|
return this.request('post', path, json, options);
|
|
27
53
|
}
|
|
@@ -43,10 +69,40 @@ export function isNotFoundError(error) {
|
|
|
43
69
|
const cause = error?.cause;
|
|
44
70
|
return cause?.response?.statusCode === 404;
|
|
45
71
|
}
|
|
72
|
+
// A 404 a Loopress controller raises on purpose, identified by its JSON body's `error` field
|
|
73
|
+
// (e.g. `{"error": "composer.lock not found"}` from ComposerController). Narrower than
|
|
74
|
+
// isNotFoundError: a bare 404 also covers the route being absent because the plugin isn't
|
|
75
|
+
// installed or predates the feature, which callers must still surface normally.
|
|
76
|
+
export function isApplicative404(error, marker) {
|
|
77
|
+
if (!isNotFoundError(error))
|
|
78
|
+
return false;
|
|
79
|
+
const body = error.cause?.response?.body;
|
|
80
|
+
if (!body)
|
|
81
|
+
return false;
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(body).error === marker;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
46
89
|
export function isTimeoutError(error) {
|
|
47
90
|
const cause = error?.cause;
|
|
48
91
|
return cause?.name === 'TimeoutError';
|
|
49
92
|
}
|
|
93
|
+
// WordPress core answers 400 with this `code` when `?page=N` is past the last page of a
|
|
94
|
+
// collection, which is how `WpClient.getAll` knows it has walked them all.
|
|
95
|
+
export function isInvalidPageNumber(error) {
|
|
96
|
+
const body = error.cause?.response?.body;
|
|
97
|
+
if (!body)
|
|
98
|
+
return false;
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(body).code === 'rest_post_invalid_page_number';
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
50
106
|
export function formatWpError(error, url, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
51
107
|
const err = error;
|
|
52
108
|
const status = err.response?.statusCode;
|
|
@@ -23,18 +23,22 @@ export interface LoopressProjectConfiguration {
|
|
|
23
23
|
* Directory where forms (WPForms today) are read from and written to, relative to rootDir.
|
|
24
24
|
*/
|
|
25
25
|
formDir?: string;
|
|
26
|
-
/**
|
|
27
|
-
* Directory where WordPress pages are read from and written to, relative to rootDir.
|
|
28
|
-
*/
|
|
29
|
-
pageDir?: string;
|
|
30
26
|
/**
|
|
31
27
|
* Directory where custom API route files are read from and written to, relative to rootDir.
|
|
32
28
|
*/
|
|
33
29
|
apiDir?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Directory where hook files (WordPress actions, filters, and scheduled tasks) are read from and written to, relative to rootDir.
|
|
32
|
+
*/
|
|
33
|
+
hooksDir?: string;
|
|
34
34
|
/**
|
|
35
35
|
* Directory where single-page app bundles are read from and written to, relative to rootDir. Each subdirectory is one app: a loopress.app.json plus a built dist/ folder.
|
|
36
36
|
*/
|
|
37
37
|
appsDir?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Directory where explicitly tracked WordPress options (wp_options rows) are read from and written to, relative to rootDir. Only options added via `lps option add` live here, this is never the whole options table.
|
|
40
|
+
*/
|
|
41
|
+
optionsDir?: string;
|
|
38
42
|
/**
|
|
39
43
|
* Project identifier from the global Loopress config. When set, takes precedence over the currently active project in the global config.
|
|
40
44
|
*/
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const OPTIONS_ENDPOINT = "loopress/v1/options";
|
|
2
|
+
export declare function optionEndpoint(name: string): string;
|
|
3
|
+
export declare const RESERVED_OPTION_NAMES: readonly ["active_plugins", "stylesheet", "template"];
|
|
4
|
+
export declare const READONLY_BY_DEFAULT_OPTION_NAMES: readonly ["siteurl", "home", "db_version", "cron", "rewrite_rules", "WPLANG"];
|
|
5
|
+
export type LocalOption = {
|
|
6
|
+
autoload: string;
|
|
7
|
+
name: string;
|
|
8
|
+
readonly?: boolean;
|
|
9
|
+
value: unknown;
|
|
10
|
+
};
|
|
11
|
+
export type RemoteOption = {
|
|
12
|
+
autoload: string;
|
|
13
|
+
name: string;
|
|
14
|
+
value: unknown;
|
|
15
|
+
};
|
|
16
|
+
export type ListedOption = {
|
|
17
|
+
autoload: string;
|
|
18
|
+
confirmed: boolean;
|
|
19
|
+
core: boolean;
|
|
20
|
+
guess: null | string;
|
|
21
|
+
name: string;
|
|
22
|
+
pluginName: null | string;
|
|
23
|
+
};
|
|
24
|
+
export declare function isReservedOptionName(name: string): boolean;
|
|
25
|
+
export declare function defaultReadonlyFor(name: string): boolean;
|
|
26
|
+
export declare function optionFileName(name: string): string;
|
|
27
|
+
export declare function partitionByReadonly(tracked: LocalOption[]): {
|
|
28
|
+
skipped: LocalOption[];
|
|
29
|
+
writable: LocalOption[];
|
|
30
|
+
};
|
|
31
|
+
export declare function parseLocalOption(raw: string): LocalOption;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const OPTIONS_ENDPOINT = 'loopress/v1/options';
|
|
2
|
+
export function optionEndpoint(name) {
|
|
3
|
+
return `${OPTIONS_ENDPOINT}/${encodeURIComponent(name)}`;
|
|
4
|
+
}
|
|
5
|
+
// Mirrors OptionsService::RESERVED_NAMES on the WordPress side (the actual, unbypassable
|
|
6
|
+
// enforcement lives there). Checked here too so `option add` fails fast with a clear pointer to
|
|
7
|
+
// the right command, instead of a round trip just to hit the server's 409.
|
|
8
|
+
export const RESERVED_OPTION_NAMES = ['active_plugins', 'stylesheet', 'template'];
|
|
9
|
+
// Options WordPress core owns or regenerates itself: legitimate to track for visibility (`diff`
|
|
10
|
+
// still shows drift), but never something `push` should overwrite by default. siteurl/home in
|
|
11
|
+
// particular: pushing a value copied from another environment would repoint the target site's
|
|
12
|
+
// own URLs, potentially locking out wp-admin.
|
|
13
|
+
export const READONLY_BY_DEFAULT_OPTION_NAMES = ['siteurl', 'home', 'db_version', 'cron', 'rewrite_rules', 'WPLANG'];
|
|
14
|
+
export function isReservedOptionName(name) {
|
|
15
|
+
return RESERVED_OPTION_NAMES.includes(name);
|
|
16
|
+
}
|
|
17
|
+
export function defaultReadonlyFor(name) {
|
|
18
|
+
return READONLY_BY_DEFAULT_OPTION_NAMES.includes(name);
|
|
19
|
+
}
|
|
20
|
+
export function optionFileName(name) {
|
|
21
|
+
return `${name}.json`;
|
|
22
|
+
}
|
|
23
|
+
// Splits tracked options into what `push` may write and what it must skip: readonly is a local
|
|
24
|
+
// policy flag (see READONLY_BY_DEFAULT_OPTION_NAMES), never something WordPress itself enforces.
|
|
25
|
+
export function partitionByReadonly(tracked) {
|
|
26
|
+
const writable = [];
|
|
27
|
+
const skipped = [];
|
|
28
|
+
for (const option of tracked)
|
|
29
|
+
(option.readonly ? skipped : writable).push(option);
|
|
30
|
+
return { skipped, writable };
|
|
31
|
+
}
|
|
32
|
+
export function parseLocalOption(raw) {
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
35
|
+
throw new Error('not a JSON object');
|
|
36
|
+
}
|
|
37
|
+
const { autoload, name, value } = parsed;
|
|
38
|
+
if (typeof name !== 'string' || name === '') {
|
|
39
|
+
throw new Error('missing a "name" string');
|
|
40
|
+
}
|
|
41
|
+
if (typeof autoload !== 'string') {
|
|
42
|
+
throw new TypeError('missing an "autoload" string');
|
|
43
|
+
}
|
|
44
|
+
// `value` itself is deliberately untyped (a WP option can hold any JSON-safe value, including
|
|
45
|
+
// null); only the key's presence is checked, so a hand-edited file that dropped it entirely is
|
|
46
|
+
// rejected here rather than silently pushing an `undefined` value to WordPress.
|
|
47
|
+
if (value === undefined) {
|
|
48
|
+
throw new Error('missing a "value" field');
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type LoopressLocalConfig } from './loopress-config.js';
|
|
2
|
+
export declare const RESOURCE_DIR_DEFAULTS: {
|
|
3
|
+
readonly acf: "acf";
|
|
4
|
+
readonly api: "api";
|
|
5
|
+
readonly apps: "apps";
|
|
6
|
+
readonly form: "forms";
|
|
7
|
+
readonly hooks: "hooks";
|
|
8
|
+
readonly options: "options";
|
|
9
|
+
readonly seo: "seo";
|
|
10
|
+
readonly snippets: "snippets";
|
|
11
|
+
};
|
|
12
|
+
export type ResourceDirKind = keyof typeof RESOURCE_DIR_DEFAULTS;
|
|
13
|
+
export declare function resolveResourceDir(kind: ResourceDirKind, localConfig: LoopressLocalConfig, override?: string): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
// The default subdirectory for each file-backed resource, relative to rootDir. `lps init`
|
|
3
|
+
// writes these same defaults into loopress.json, keep the two in sync.
|
|
4
|
+
export const RESOURCE_DIR_DEFAULTS = {
|
|
5
|
+
acf: 'acf',
|
|
6
|
+
api: 'api',
|
|
7
|
+
apps: 'apps',
|
|
8
|
+
form: 'forms',
|
|
9
|
+
hooks: 'hooks',
|
|
10
|
+
options: 'options',
|
|
11
|
+
seo: 'seo',
|
|
12
|
+
snippets: 'snippets',
|
|
13
|
+
};
|
|
14
|
+
const CONFIG_KEY = {
|
|
15
|
+
acf: 'acfDir',
|
|
16
|
+
api: 'apiDir',
|
|
17
|
+
apps: 'appsDir',
|
|
18
|
+
form: 'formDir',
|
|
19
|
+
hooks: 'hooksDir',
|
|
20
|
+
options: 'optionsDir',
|
|
21
|
+
seo: 'seoDir',
|
|
22
|
+
snippets: 'snippetsDir',
|
|
23
|
+
};
|
|
24
|
+
// Resolves where a resource's files live: an explicit override wins, otherwise the
|
|
25
|
+
// loopress.json `<kind>Dir` setting, otherwise the built-in default, all under `rootDir`.
|
|
26
|
+
// Shared by every pull/push command (via LoopressCommand's resolve*Path helpers) and by
|
|
27
|
+
// `lps diff`, so the resolution rule has a single home.
|
|
28
|
+
export function resolveResourceDir(kind, localConfig, override) {
|
|
29
|
+
if (override)
|
|
30
|
+
return override;
|
|
31
|
+
const rootDir = localConfig.rootDir ?? '.';
|
|
32
|
+
const configured = localConfig[CONFIG_KEY[kind]];
|
|
33
|
+
return join(rootDir, configured ?? RESOURCE_DIR_DEFAULTS[kind]);
|
|
34
|
+
}
|