@loopress/cli 0.19.0 → 0.20.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 +158 -37
- package/dist/commands/api/publish.d.ts +9 -0
- package/dist/commands/api/publish.js +60 -0
- package/dist/commands/api/push.d.ts +1 -0
- package/dist/commands/api/push.js +19 -0
- package/dist/commands/composer/pull.js +38 -3
- package/dist/commands/init.js +1 -0
- package/dist/commands/page/list.d.ts +9 -0
- package/dist/commands/page/list.js +26 -0
- package/dist/commands/page/pull.d.ts +13 -0
- package/dist/commands/page/pull.js +65 -0
- package/dist/commands/page/push.d.ts +16 -0
- package/dist/commands/page/push.js +150 -0
- package/dist/commands/project/config.d.ts +1 -0
- package/dist/commands/project/config.js +28 -0
- package/dist/commands/project/rotate.d.ts +7 -0
- package/dist/commands/project/rotate.js +20 -0
- package/dist/lib/base.d.ts +3 -0
- package/dist/lib/base.js +42 -10
- package/dist/lib/bootstrap-full-install.d.ts +10 -0
- package/dist/lib/bootstrap-full-install.js +66 -0
- package/dist/lib/browser-launch.d.ts +8 -0
- package/dist/lib/browser-launch.js +20 -0
- package/dist/lib/github-release.d.ts +11 -0
- package/dist/lib/github-release.js +37 -0
- package/dist/lib/plugin-detection.d.ts +8 -0
- package/dist/lib/plugin-detection.js +11 -0
- package/dist/lib/rotate-app-password.d.ts +6 -0
- package/dist/lib/rotate-app-password.js +33 -0
- package/dist/lib/temp-admin.d.ts +19 -0
- package/dist/lib/temp-admin.js +46 -0
- package/dist/lib/wp-client.d.ts +1 -0
- package/dist/lib/wp-client.js +7 -1
- package/dist/lib/wp-site-diagnostic.d.ts +9 -2
- package/dist/lib/wp-site-diagnostic.js +13 -23
- package/dist/types/project-config.generated.d.ts +4 -0
- package/dist/utils/page-format.d.ts +7 -0
- package/dist/utils/page-format.js +78 -0
- package/oclif.manifest.json +252 -1
- package/package.json +8 -7
- package/schemas/project-config.schema.json +5 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { WpClient } from './wp-client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Checks `wp/v2/plugins` directly rather than through `parseInstalledPlugins`
|
|
4
|
+
* (`src/utils/plugins.ts`), which deliberately filters Loopress's own plugin slugs out of its
|
|
5
|
+
* result so `plugin push` never tries to manage itself. This is the one place that needs to
|
|
6
|
+
* see Loopress Full itself.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isLoopressFullActive(wp: WpClient): Promise<boolean>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const FULL_PLUGIN_PREFIX = 'loopress-full/';
|
|
2
|
+
/**
|
|
3
|
+
* Checks `wp/v2/plugins` directly rather than through `parseInstalledPlugins`
|
|
4
|
+
* (`src/utils/plugins.ts`), which deliberately filters Loopress's own plugin slugs out of its
|
|
5
|
+
* result so `plugin push` never tries to manage itself. This is the one place that needs to
|
|
6
|
+
* see Loopress Full itself.
|
|
7
|
+
*/
|
|
8
|
+
export async function isLoopressFullActive(wp) {
|
|
9
|
+
const plugins = await wp.get('wp/v2/plugins');
|
|
10
|
+
return plugins.some((plugin) => plugin.plugin.startsWith(FULL_PLUGIN_PREFIX) && plugin.status === 'active');
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { EnvironmentConfig } from '../types/config.js';
|
|
2
|
+
export declare const APP_PASSWORD_MAX_AGE_MS: number;
|
|
3
|
+
export declare function isAppPasswordStale(addedAt: string): boolean;
|
|
4
|
+
export declare function rotateAppPassword(env: EnvironmentConfig & {
|
|
5
|
+
token: string;
|
|
6
|
+
}): Promise<EnvironmentConfig>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { WpClient } from './wp-client.js';
|
|
2
|
+
const APPLICATION_PASSWORDS_PATH = 'wp/v2/users/me/application-passwords';
|
|
3
|
+
export const APP_PASSWORD_MAX_AGE_MS = 90 * 24 * 60 * 60 * 1000;
|
|
4
|
+
export function isAppPasswordStale(addedAt) {
|
|
5
|
+
return Date.now() - new Date(addedAt).getTime() > APP_PASSWORD_MAX_AGE_MS;
|
|
6
|
+
}
|
|
7
|
+
// Create the new credential, confirm it actually authenticates on its own, only then revoke
|
|
8
|
+
// the old one. Reversing this order risks locking the user out if the new one is somehow bad.
|
|
9
|
+
export async function rotateAppPassword(env) {
|
|
10
|
+
const [user] = env.token.split(':');
|
|
11
|
+
const oldClient = new WpClient(env.url, env.token);
|
|
12
|
+
const old = await oldClient.get(`${APPLICATION_PASSWORDS_PATH}/introspect`);
|
|
13
|
+
const created = await oldClient.post(APPLICATION_PASSWORDS_PATH, { name: 'Loopress' });
|
|
14
|
+
if (!created.password) {
|
|
15
|
+
throw new Error(`WordPress did not return a password when creating the new application password on ${env.url}.`);
|
|
16
|
+
}
|
|
17
|
+
const newToken = `${user}:${created.password}`;
|
|
18
|
+
const newClient = new WpClient(env.url, newToken);
|
|
19
|
+
try {
|
|
20
|
+
await newClient.get(`${APPLICATION_PASSWORDS_PATH}/introspect`);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
// The new credential exists on WordPress but was never confirmed to work: clean it up with
|
|
24
|
+
// the still-valid old credentials instead of leaving an orphan behind. maybeAutoRotate
|
|
25
|
+
// retries on every subsequent run, so a persistent failure here would otherwise pile up
|
|
26
|
+
// one abandoned 'Loopress' entry per attempt. Best-effort: a failure here shouldn't hide
|
|
27
|
+
// the original, more informative error.
|
|
28
|
+
await oldClient.delete(`${APPLICATION_PASSWORDS_PATH}/${created.uuid}`).catch(() => { });
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
await newClient.delete(`${APPLICATION_PASSWORDS_PATH}/${old.uuid}`);
|
|
32
|
+
return { ...env, addedAt: new Date().toISOString(), token: newToken };
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { WpClient } from './wp-client.js';
|
|
2
|
+
export interface TempAdmin {
|
|
3
|
+
id: number;
|
|
4
|
+
password: string;
|
|
5
|
+
username: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Creates a temporary WordPress administrator, the only way to obtain `install_plugins` when
|
|
9
|
+
* all the CLI has is an application password. `.invalid` is the RFC 2606 reserved TLD for
|
|
10
|
+
* addresses guaranteed not to be real, appropriate for an account that has no real mailbox.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createTempAdmin(wp: WpClient): Promise<TempAdmin>;
|
|
13
|
+
/**
|
|
14
|
+
* Deletes the temporary admin, reassigning any content it might own to the real user behind
|
|
15
|
+
* the app password. Failure here throws rather than warns, naming the leftover account: a
|
|
16
|
+
* cleanup that silently fails would leave a live administrator with a random password on the
|
|
17
|
+
* target site, discoverable only by someone reading logs closely.
|
|
18
|
+
*/
|
|
19
|
+
export declare function deleteTempAdmin(wp: WpClient, admin: TempAdmin): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { isNotFoundError } from './wp-client.js';
|
|
3
|
+
// Only role that carries `install_plugins`/`activate_plugins`; there's nothing narrower to grant.
|
|
4
|
+
const TEMP_ADMIN_ROLE = 'administrator';
|
|
5
|
+
/**
|
|
6
|
+
* Creates a temporary WordPress administrator, the only way to obtain `install_plugins` when
|
|
7
|
+
* all the CLI has is an application password. `.invalid` is the RFC 2606 reserved TLD for
|
|
8
|
+
* addresses guaranteed not to be real, appropriate for an account that has no real mailbox.
|
|
9
|
+
*/
|
|
10
|
+
export async function createTempAdmin(wp) {
|
|
11
|
+
const username = `lps-temp-${Date.now().toString(36)}`;
|
|
12
|
+
const password = randomBytes(24).toString('base64url');
|
|
13
|
+
const user = await wp.post('wp/v2/users', {
|
|
14
|
+
email: `${username}@lps-temp.invalid`,
|
|
15
|
+
password,
|
|
16
|
+
roles: [TEMP_ADMIN_ROLE],
|
|
17
|
+
username,
|
|
18
|
+
});
|
|
19
|
+
return { id: user.id, password, username };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Deletes the temporary admin, reassigning any content it might own to the real user behind
|
|
23
|
+
* the app password. Failure here throws rather than warns, naming the leftover account: a
|
|
24
|
+
* cleanup that silently fails would leave a live administrator with a random password on the
|
|
25
|
+
* target site, discoverable only by someone reading logs closely.
|
|
26
|
+
*/
|
|
27
|
+
export async function deleteTempAdmin(wp, admin) {
|
|
28
|
+
const me = await wp.get('wp/v2/users/me');
|
|
29
|
+
try {
|
|
30
|
+
await wp.delete(`wp/v2/users/${admin.id}?reassign=${me.id}&force=true`);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new Error(`Failed to remove the temporary admin account "${admin.username}" (id ${admin.id}) from the site. Remove it manually from wp-admin.`, { cause: error });
|
|
34
|
+
}
|
|
35
|
+
const stillExists = await wp
|
|
36
|
+
.get(`wp/v2/users/${admin.id}`)
|
|
37
|
+
.then(() => true)
|
|
38
|
+
.catch((error) => {
|
|
39
|
+
if (isNotFoundError(error))
|
|
40
|
+
return false;
|
|
41
|
+
throw error;
|
|
42
|
+
});
|
|
43
|
+
if (stillExists) {
|
|
44
|
+
throw new Error(`Temporary admin account "${admin.username}" (id ${admin.id}) still exists after deletion. Remove it manually from wp-admin.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/lib/wp-client.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare class WpClient {
|
|
|
7
7
|
private readonly siteUrl;
|
|
8
8
|
private readonly client;
|
|
9
9
|
constructor(siteUrl: string, token: string);
|
|
10
|
+
delete<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
|
|
10
11
|
get<T>(path: string, options?: RequestOptions): Promise<T>;
|
|
11
12
|
post<T = unknown>(path: string, json?: Record<string, unknown>, options?: RequestOptions): Promise<T>;
|
|
12
13
|
put<T = unknown>(path: string, json?: Record<string, unknown>, options?: RequestOptions): Promise<T>;
|
package/dist/lib/wp-client.js
CHANGED
|
@@ -15,6 +15,9 @@ export class WpClient {
|
|
|
15
15
|
timeout: { request: REQUEST_TIMEOUT_MS },
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
+
async delete(path, options) {
|
|
19
|
+
return this.request('delete', path, undefined, options);
|
|
20
|
+
}
|
|
18
21
|
async get(path, options) {
|
|
19
22
|
return this.request('get', path, undefined, options);
|
|
20
23
|
}
|
|
@@ -50,7 +53,10 @@ export function formatWpError(error, url, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
50
53
|
return `Authentication failed (${status}) on ${url}. Check your credentials with \`lps project config\`.`;
|
|
51
54
|
}
|
|
52
55
|
if (status === 404) {
|
|
53
|
-
|
|
56
|
+
const reason = extractServerErrorMessage(err.response?.body);
|
|
57
|
+
return reason
|
|
58
|
+
? `Request failed (404) on ${url}: ${reason}`
|
|
59
|
+
: `Endpoint not found (404) on ${url}. Is the required plugin installed and up to date on the site?`;
|
|
54
60
|
}
|
|
55
61
|
if (status !== undefined) {
|
|
56
62
|
const reason = extractServerErrorMessage(err.response?.body);
|
|
@@ -7,7 +7,14 @@ export type DiagnosticResult = {
|
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
9
|
* Pre-flight checks run before starting the browser authorization flow, so failures
|
|
10
|
-
* (unreachable site, blocked REST API,
|
|
11
|
-
* message instead of a confusing timeout once the browser is already open.
|
|
10
|
+
* (unreachable site, blocked REST API, Application Passwords disabled) surface as an
|
|
11
|
+
* actionable message instead of a confusing timeout once the browser is already open.
|
|
12
|
+
*
|
|
13
|
+
* Whether Application Passwords are enabled can only be read from the `wp-json/` index:
|
|
14
|
+
* WordPress adds `authentication['application-passwords']` there when the feature is on
|
|
15
|
+
* (WP core, rest_add_application_passwords_to_index). The authorize-application.php page
|
|
16
|
+
* can't be probed for this instead, it sits behind the admin login wall, so an unauthenticated
|
|
17
|
+
* request just gets redirected to wp-login.php and never reaches the check that would say
|
|
18
|
+
* whether the feature is disabled.
|
|
12
19
|
*/
|
|
13
20
|
export declare function diagnoseWpSite(siteUrl: string): Promise<DiagnosticResult>;
|
|
@@ -2,12 +2,20 @@ import got from 'got';
|
|
|
2
2
|
export const REQUEST_TIMEOUT_MS = 10_000;
|
|
3
3
|
/**
|
|
4
4
|
* Pre-flight checks run before starting the browser authorization flow, so failures
|
|
5
|
-
* (unreachable site, blocked REST API,
|
|
6
|
-
* message instead of a confusing timeout once the browser is already open.
|
|
5
|
+
* (unreachable site, blocked REST API, Application Passwords disabled) surface as an
|
|
6
|
+
* actionable message instead of a confusing timeout once the browser is already open.
|
|
7
|
+
*
|
|
8
|
+
* Whether Application Passwords are enabled can only be read from the `wp-json/` index:
|
|
9
|
+
* WordPress adds `authentication['application-passwords']` there when the feature is on
|
|
10
|
+
* (WP core, rest_add_application_passwords_to_index). The authorize-application.php page
|
|
11
|
+
* can't be probed for this instead, it sits behind the admin login wall, so an unauthenticated
|
|
12
|
+
* request just gets redirected to wp-login.php and never reaches the check that would say
|
|
13
|
+
* whether the feature is disabled.
|
|
7
14
|
*/
|
|
8
15
|
export async function diagnoseWpSite(siteUrl) {
|
|
16
|
+
let index;
|
|
9
17
|
try {
|
|
10
|
-
await got.get(`${siteUrl}/wp-json/`, { timeout: { request: REQUEST_TIMEOUT_MS } });
|
|
18
|
+
index = await got.get(`${siteUrl}/wp-json/`, { timeout: { request: REQUEST_TIMEOUT_MS } }).json();
|
|
11
19
|
}
|
|
12
20
|
catch (error) {
|
|
13
21
|
return {
|
|
@@ -15,28 +23,10 @@ export async function diagnoseWpSite(siteUrl) {
|
|
|
15
23
|
reason: `Could not reach the WordPress REST API at ${siteUrl}/wp-json/. The site may be unreachable, or a security plugin may be blocking it. (${describe(error)})`,
|
|
16
24
|
};
|
|
17
25
|
}
|
|
18
|
-
|
|
19
|
-
const response = await got.head(`${siteUrl}/wp-admin/authorize-application.php`, {
|
|
20
|
-
throwHttpErrors: false,
|
|
21
|
-
timeout: { request: REQUEST_TIMEOUT_MS },
|
|
22
|
-
});
|
|
23
|
-
if (response.statusCode === 404) {
|
|
24
|
-
return {
|
|
25
|
-
ok: false,
|
|
26
|
-
reason: `The application authorization page was not found on ${siteUrl}. This WordPress site may be older than 5.6, or the feature may be disabled by a plugin.`,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
if (response.statusCode >= 400) {
|
|
30
|
-
return {
|
|
31
|
-
ok: false,
|
|
32
|
-
reason: `The application authorization page at ${siteUrl}/wp-admin/authorize-application.php returned an error (HTTP ${response.statusCode}).`,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
26
|
+
if (!index?.authentication?.['application-passwords']) {
|
|
37
27
|
return {
|
|
38
28
|
ok: false,
|
|
39
|
-
reason: `
|
|
29
|
+
reason: `Application Passwords are not available on ${siteUrl}. The site may be older than WordPress 5.6, require HTTPS, or have the feature disabled by a plugin or filter.`,
|
|
40
30
|
};
|
|
41
31
|
}
|
|
42
32
|
return { ok: true };
|
|
@@ -23,6 +23,10 @@ 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;
|
|
26
30
|
/**
|
|
27
31
|
* Directory where custom API route files are read from and written to, relative to rootDir.
|
|
28
32
|
*/
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const PAGE_ENDPOINT = "wp/v2/pages";
|
|
2
|
+
export declare const PAGE_LIST_QUERY = "per_page=100";
|
|
3
|
+
export declare function getPageId(data: Record<string, unknown>): null | number;
|
|
4
|
+
export declare function getPageTitle(data: Record<string, unknown>): string;
|
|
5
|
+
export declare function pageFileBase(id: number, title: string): string;
|
|
6
|
+
export declare function pickPageMeta(data: Record<string, unknown>): Record<string, unknown>;
|
|
7
|
+
export declare function getPageContent(data: Record<string, unknown>): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { toSlug } from './to-slug.js';
|
|
2
|
+
// WordPress core's own REST endpoint (`wp/v2/pages`), unlike acf/form/seo/snippet there is no
|
|
3
|
+
// Loopress-plugin controller behind this one, same principle as `wp/v2/plugins` in plugin push/pull.
|
|
4
|
+
export const PAGE_ENDPOINT = 'wp/v2/pages';
|
|
5
|
+
// ponytail: fixed cap, not a paged loop; fine for typical page counts, revisit with real
|
|
6
|
+
// pagination (X-WP-TotalPages) if a site ever has more than 100 pages.
|
|
7
|
+
export const PAGE_LIST_QUERY = 'per_page=100';
|
|
8
|
+
export function getPageId(data) {
|
|
9
|
+
const id = Number(data.id);
|
|
10
|
+
return Number.isInteger(id) && id > 0 ? id : null;
|
|
11
|
+
}
|
|
12
|
+
// `title`/`content`/`excerpt` come back as `{raw, rendered}` from `?context=edit` (what `page
|
|
13
|
+
// pull` requests) but round-trip fine as plain strings too for a hand-created file, same
|
|
14
|
+
// looseness as getFormTitle in form-format.ts.
|
|
15
|
+
export function getPageTitle(data) {
|
|
16
|
+
const { title } = data;
|
|
17
|
+
if (typeof title === 'string' && title.trim() !== '')
|
|
18
|
+
return title;
|
|
19
|
+
if (title && typeof title === 'object') {
|
|
20
|
+
const { raw, rendered } = title;
|
|
21
|
+
if (typeof raw === 'string' && raw.trim() !== '')
|
|
22
|
+
return raw;
|
|
23
|
+
if (typeof rendered === 'string' && rendered.trim() !== '')
|
|
24
|
+
return rendered;
|
|
25
|
+
}
|
|
26
|
+
return '(untitled)';
|
|
27
|
+
}
|
|
28
|
+
export function pageFileBase(id, title) {
|
|
29
|
+
return `${id}-${toSlug(title, 'untitled')}`;
|
|
30
|
+
}
|
|
31
|
+
// The fields WordPress core actually accepts on write to `wp/v2/pages` (confirmed via
|
|
32
|
+
// `OPTIONS wp/v2/pages`), plus `id` for local identity. A GET response carries a lot more:
|
|
33
|
+
// `guid`, `link`, `modified`/`modified_gmt`, `permalink_template`, `generated_slug`,
|
|
34
|
+
// `class_list`, `type`, `_links`, all of it readonly/computed by WordPress, most of it
|
|
35
|
+
// domain- or revision-specific. `_links` alone is the bulk of the file and, like
|
|
36
|
+
// `modified`/`modified_gmt`, changes on every edit regardless of whether the page's actual
|
|
37
|
+
// content changed, which would make every `page pull` produce a spurious git diff if kept.
|
|
38
|
+
const PAGE_META_FIELDS = [
|
|
39
|
+
'id',
|
|
40
|
+
'date',
|
|
41
|
+
'date_gmt',
|
|
42
|
+
'slug',
|
|
43
|
+
'status',
|
|
44
|
+
'password',
|
|
45
|
+
'parent',
|
|
46
|
+
'title',
|
|
47
|
+
'author',
|
|
48
|
+
'excerpt',
|
|
49
|
+
'featured_media',
|
|
50
|
+
'comment_status',
|
|
51
|
+
'ping_status',
|
|
52
|
+
'menu_order',
|
|
53
|
+
'meta',
|
|
54
|
+
'template',
|
|
55
|
+
];
|
|
56
|
+
export function pickPageMeta(data) {
|
|
57
|
+
const meta = {};
|
|
58
|
+
for (const field of PAGE_META_FIELDS) {
|
|
59
|
+
if (field in data)
|
|
60
|
+
meta[field] = data[field];
|
|
61
|
+
}
|
|
62
|
+
return meta;
|
|
63
|
+
}
|
|
64
|
+
// `content` comes back as `{raw, rendered}` from `?context=edit` (what `page pull` requests);
|
|
65
|
+
// a hand-created file only has the plain string. This is the one field split out into its own
|
|
66
|
+
// `.html` file by pull/push: raw Gutenberg block markup escaped inside a JSON string is neither
|
|
67
|
+
// readable nor safely hand-editable, same reasoning as the code/meta split in snippet-format.ts.
|
|
68
|
+
export function getPageContent(data) {
|
|
69
|
+
const { content } = data;
|
|
70
|
+
if (typeof content === 'string')
|
|
71
|
+
return content;
|
|
72
|
+
if (content && typeof content === 'object') {
|
|
73
|
+
const { raw } = content;
|
|
74
|
+
if (typeof raw === 'string')
|
|
75
|
+
return raw;
|
|
76
|
+
}
|
|
77
|
+
return '';
|
|
78
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -439,6 +439,36 @@
|
|
|
439
439
|
"list.js"
|
|
440
440
|
]
|
|
441
441
|
},
|
|
442
|
+
"api:publish": {
|
|
443
|
+
"aliases": [],
|
|
444
|
+
"args": {
|
|
445
|
+
"path": {
|
|
446
|
+
"description": "Path to api directory (overrides project config)",
|
|
447
|
+
"name": "path"
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
"description": "Publish custom API routes to your Loopress account so they can be deployed to other projects. Does not touch any WordPress site.",
|
|
451
|
+
"examples": [
|
|
452
|
+
"$ lps api publish",
|
|
453
|
+
"$ lps api publish --path ./api"
|
|
454
|
+
],
|
|
455
|
+
"flags": {},
|
|
456
|
+
"hasDynamicHelp": false,
|
|
457
|
+
"hiddenAliases": [],
|
|
458
|
+
"id": "api:publish",
|
|
459
|
+
"pluginAlias": "@loopress/cli",
|
|
460
|
+
"pluginName": "@loopress/cli",
|
|
461
|
+
"pluginType": "core",
|
|
462
|
+
"strict": true,
|
|
463
|
+
"enableJsonFlag": false,
|
|
464
|
+
"isESM": true,
|
|
465
|
+
"relativePath": [
|
|
466
|
+
"dist",
|
|
467
|
+
"commands",
|
|
468
|
+
"api",
|
|
469
|
+
"publish.js"
|
|
470
|
+
]
|
|
471
|
+
},
|
|
442
472
|
"api:pull": {
|
|
443
473
|
"aliases": [],
|
|
444
474
|
"args": {
|
|
@@ -888,6 +918,178 @@
|
|
|
888
918
|
"push.js"
|
|
889
919
|
]
|
|
890
920
|
},
|
|
921
|
+
"page:list": {
|
|
922
|
+
"aliases": [],
|
|
923
|
+
"args": {},
|
|
924
|
+
"description": "List pages from WordPress",
|
|
925
|
+
"examples": [
|
|
926
|
+
"$ lps page list"
|
|
927
|
+
],
|
|
928
|
+
"flags": {
|
|
929
|
+
"env": {
|
|
930
|
+
"description": "Target environment by name, overriding the globally active one (lps project switch)",
|
|
931
|
+
"name": "env",
|
|
932
|
+
"hasDynamicHelp": false,
|
|
933
|
+
"multiple": false,
|
|
934
|
+
"type": "option"
|
|
935
|
+
},
|
|
936
|
+
"json": {
|
|
937
|
+
"char": "j",
|
|
938
|
+
"description": "Output in JSON format",
|
|
939
|
+
"name": "json",
|
|
940
|
+
"allowNo": false,
|
|
941
|
+
"type": "boolean"
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
"hasDynamicHelp": false,
|
|
945
|
+
"hiddenAliases": [],
|
|
946
|
+
"id": "page:list",
|
|
947
|
+
"pluginAlias": "@loopress/cli",
|
|
948
|
+
"pluginName": "@loopress/cli",
|
|
949
|
+
"pluginType": "core",
|
|
950
|
+
"strict": true,
|
|
951
|
+
"enableJsonFlag": false,
|
|
952
|
+
"dryRunFlag": {
|
|
953
|
+
"dry-run": {
|
|
954
|
+
"char": "d",
|
|
955
|
+
"description": "Show what would change without making changes",
|
|
956
|
+
"allowNo": false,
|
|
957
|
+
"type": "boolean"
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
"yesFlag": {
|
|
961
|
+
"yes": {
|
|
962
|
+
"char": "y",
|
|
963
|
+
"description": "Answer yes to confirmation prompts",
|
|
964
|
+
"allowNo": false,
|
|
965
|
+
"type": "boolean"
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
"isESM": true,
|
|
969
|
+
"relativePath": [
|
|
970
|
+
"dist",
|
|
971
|
+
"commands",
|
|
972
|
+
"page",
|
|
973
|
+
"list.js"
|
|
974
|
+
]
|
|
975
|
+
},
|
|
976
|
+
"page:pull": {
|
|
977
|
+
"aliases": [],
|
|
978
|
+
"args": {
|
|
979
|
+
"path": {
|
|
980
|
+
"description": "Path to pages directory (overrides project config)",
|
|
981
|
+
"name": "path"
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
"description": "Pull pages from WordPress",
|
|
985
|
+
"examples": [
|
|
986
|
+
"$ lps page pull"
|
|
987
|
+
],
|
|
988
|
+
"flags": {
|
|
989
|
+
"env": {
|
|
990
|
+
"description": "Target environment by name, overriding the globally active one (lps project switch)",
|
|
991
|
+
"name": "env",
|
|
992
|
+
"hasDynamicHelp": false,
|
|
993
|
+
"multiple": false,
|
|
994
|
+
"type": "option"
|
|
995
|
+
},
|
|
996
|
+
"dry-run": {
|
|
997
|
+
"char": "d",
|
|
998
|
+
"description": "Show what would change without making changes",
|
|
999
|
+
"name": "dry-run",
|
|
1000
|
+
"allowNo": false,
|
|
1001
|
+
"type": "boolean"
|
|
1002
|
+
},
|
|
1003
|
+
"yes": {
|
|
1004
|
+
"char": "y",
|
|
1005
|
+
"description": "Answer yes to confirmation prompts",
|
|
1006
|
+
"name": "yes",
|
|
1007
|
+
"allowNo": false,
|
|
1008
|
+
"type": "boolean"
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
1011
|
+
"hasDynamicHelp": false,
|
|
1012
|
+
"hiddenAliases": [],
|
|
1013
|
+
"id": "page:pull",
|
|
1014
|
+
"pluginAlias": "@loopress/cli",
|
|
1015
|
+
"pluginName": "@loopress/cli",
|
|
1016
|
+
"pluginType": "core",
|
|
1017
|
+
"strict": true,
|
|
1018
|
+
"enableJsonFlag": false,
|
|
1019
|
+
"dryRunFlag": {
|
|
1020
|
+
"dry-run": {
|
|
1021
|
+
"char": "d",
|
|
1022
|
+
"description": "Show what would change without making changes",
|
|
1023
|
+
"allowNo": false,
|
|
1024
|
+
"type": "boolean"
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
"yesFlag": {
|
|
1028
|
+
"yes": {
|
|
1029
|
+
"char": "y",
|
|
1030
|
+
"description": "Answer yes to confirmation prompts",
|
|
1031
|
+
"allowNo": false,
|
|
1032
|
+
"type": "boolean"
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
"isESM": true,
|
|
1036
|
+
"relativePath": [
|
|
1037
|
+
"dist",
|
|
1038
|
+
"commands",
|
|
1039
|
+
"page",
|
|
1040
|
+
"pull.js"
|
|
1041
|
+
]
|
|
1042
|
+
},
|
|
1043
|
+
"page:push": {
|
|
1044
|
+
"aliases": [],
|
|
1045
|
+
"args": {
|
|
1046
|
+
"path": {
|
|
1047
|
+
"description": "Path to pages directory (overrides project config)",
|
|
1048
|
+
"name": "path"
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
"description": "Push pages to WordPress. Local files created or updated remotely are renamed on disk to the `<id>-<slug>` convention.",
|
|
1052
|
+
"examples": [
|
|
1053
|
+
"$ lps page push"
|
|
1054
|
+
],
|
|
1055
|
+
"flags": {
|
|
1056
|
+
"env": {
|
|
1057
|
+
"description": "Target environment by name, overriding the globally active one (lps project switch)",
|
|
1058
|
+
"name": "env",
|
|
1059
|
+
"hasDynamicHelp": false,
|
|
1060
|
+
"multiple": false,
|
|
1061
|
+
"type": "option"
|
|
1062
|
+
},
|
|
1063
|
+
"dry-run": {
|
|
1064
|
+
"char": "d",
|
|
1065
|
+
"description": "Show what would change without making changes",
|
|
1066
|
+
"name": "dry-run",
|
|
1067
|
+
"allowNo": false,
|
|
1068
|
+
"type": "boolean"
|
|
1069
|
+
},
|
|
1070
|
+
"yes": {
|
|
1071
|
+
"char": "y",
|
|
1072
|
+
"description": "Answer yes to confirmation prompts",
|
|
1073
|
+
"name": "yes",
|
|
1074
|
+
"allowNo": false,
|
|
1075
|
+
"type": "boolean"
|
|
1076
|
+
}
|
|
1077
|
+
},
|
|
1078
|
+
"hasDynamicHelp": false,
|
|
1079
|
+
"hiddenAliases": [],
|
|
1080
|
+
"id": "page:push",
|
|
1081
|
+
"pluginAlias": "@loopress/cli",
|
|
1082
|
+
"pluginName": "@loopress/cli",
|
|
1083
|
+
"pluginType": "core",
|
|
1084
|
+
"strict": true,
|
|
1085
|
+
"isESM": true,
|
|
1086
|
+
"relativePath": [
|
|
1087
|
+
"dist",
|
|
1088
|
+
"commands",
|
|
1089
|
+
"page",
|
|
1090
|
+
"push.js"
|
|
1091
|
+
]
|
|
1092
|
+
},
|
|
891
1093
|
"plugin:add": {
|
|
892
1094
|
"aliases": [],
|
|
893
1095
|
"args": {
|
|
@@ -1180,6 +1382,55 @@
|
|
|
1180
1382
|
"remove.js"
|
|
1181
1383
|
]
|
|
1182
1384
|
},
|
|
1385
|
+
"project:rotate": {
|
|
1386
|
+
"aliases": [],
|
|
1387
|
+
"args": {},
|
|
1388
|
+
"description": "Rotate the WordPress application password for the current (or --env) environment",
|
|
1389
|
+
"examples": [
|
|
1390
|
+
"$ lps project rotate",
|
|
1391
|
+
"$ lps project rotate --env staging"
|
|
1392
|
+
],
|
|
1393
|
+
"flags": {
|
|
1394
|
+
"env": {
|
|
1395
|
+
"description": "Target environment by name, overriding the globally active one (lps project switch)",
|
|
1396
|
+
"name": "env",
|
|
1397
|
+
"hasDynamicHelp": false,
|
|
1398
|
+
"multiple": false,
|
|
1399
|
+
"type": "option"
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
"hasDynamicHelp": false,
|
|
1403
|
+
"hiddenAliases": [],
|
|
1404
|
+
"id": "project:rotate",
|
|
1405
|
+
"pluginAlias": "@loopress/cli",
|
|
1406
|
+
"pluginName": "@loopress/cli",
|
|
1407
|
+
"pluginType": "core",
|
|
1408
|
+
"strict": true,
|
|
1409
|
+
"enableJsonFlag": false,
|
|
1410
|
+
"dryRunFlag": {
|
|
1411
|
+
"dry-run": {
|
|
1412
|
+
"char": "d",
|
|
1413
|
+
"description": "Show what would change without making changes",
|
|
1414
|
+
"allowNo": false,
|
|
1415
|
+
"type": "boolean"
|
|
1416
|
+
}
|
|
1417
|
+
},
|
|
1418
|
+
"yesFlag": {
|
|
1419
|
+
"yes": {
|
|
1420
|
+
"char": "y",
|
|
1421
|
+
"description": "Answer yes to confirmation prompts",
|
|
1422
|
+
"allowNo": false,
|
|
1423
|
+
"type": "boolean"
|
|
1424
|
+
}
|
|
1425
|
+
},
|
|
1426
|
+
"isESM": true,
|
|
1427
|
+
"relativePath": [
|
|
1428
|
+
"dist",
|
|
1429
|
+
"commands",
|
|
1430
|
+
"project",
|
|
1431
|
+
"rotate.js"
|
|
1432
|
+
]
|
|
1433
|
+
},
|
|
1183
1434
|
"project:switch": {
|
|
1184
1435
|
"aliases": [],
|
|
1185
1436
|
"args": {},
|
|
@@ -1645,5 +1896,5 @@
|
|
|
1645
1896
|
]
|
|
1646
1897
|
}
|
|
1647
1898
|
},
|
|
1648
|
-
"version": "0.
|
|
1899
|
+
"version": "0.20.1"
|
|
1649
1900
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loopress/cli",
|
|
3
3
|
"description": "CLI tool for syncing WordPress code snippets, plugins, and Composer dependencies via the REST API",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.20.1",
|
|
5
5
|
"author": "jean-smaug",
|
|
6
6
|
"bin": {
|
|
7
7
|
"loopress": "bin/run.js",
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@inquirer/prompts": "^8.5.2",
|
|
15
15
|
"@oclif/core": "^4.13.0",
|
|
16
|
-
"@oclif/plugin-help": "^6.2.
|
|
17
|
-
"@oclif/plugin-warn-if-update-available": "^3.1.
|
|
18
|
-
"@sentry/node": "^10.
|
|
16
|
+
"@oclif/plugin-help": "^6.2.55",
|
|
17
|
+
"@oclif/plugin-warn-if-update-available": "^3.1.70",
|
|
18
|
+
"@sentry/node": "^10.68.0",
|
|
19
19
|
"glob": "13.0.6",
|
|
20
20
|
"got": "^15.1.0",
|
|
21
21
|
"listr2": "^11.0.0",
|
|
22
22
|
"open": "^11.0.0",
|
|
23
|
+
"playwright-core": "^1.56.0",
|
|
23
24
|
"slugify": "^1.6.9",
|
|
24
25
|
"write-file-atomic": "^8.0.0"
|
|
25
26
|
},
|
|
@@ -31,11 +32,11 @@
|
|
|
31
32
|
"@types/node": "26.1.1",
|
|
32
33
|
"@types/write-file-atomic": "^4.0.3",
|
|
33
34
|
"@vitest/coverage-v8": "4.1.10",
|
|
34
|
-
"eslint": "^
|
|
35
|
-
"eslint-config-oclif": "^6.0.
|
|
35
|
+
"eslint": "^10.8.0",
|
|
36
|
+
"eslint-config-oclif": "^6.0.179",
|
|
36
37
|
"eslint-config-prettier": "^10",
|
|
37
38
|
"json-schema-to-typescript": "^15.0.4",
|
|
38
|
-
"oclif": "^4.23.
|
|
39
|
+
"oclif": "^4.23.29",
|
|
39
40
|
"prettier": "3.9.6",
|
|
40
41
|
"shx": "^0.4.0",
|
|
41
42
|
"ts-node": "10.9.2",
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"description": "Directory where forms (WPForms today) are read from and written to, relative to rootDir.",
|
|
35
35
|
"default": "forms"
|
|
36
36
|
},
|
|
37
|
+
"pageDir": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Directory where WordPress pages are read from and written to, relative to rootDir.",
|
|
40
|
+
"default": "pages"
|
|
41
|
+
},
|
|
37
42
|
"apiDir": {
|
|
38
43
|
"type": "string",
|
|
39
44
|
"description": "Directory where custom API route files are read from and written to, relative to rootDir.",
|