@loopress/cli 0.13.0 → 0.15.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.
Files changed (43) hide show
  1. package/README.md +96 -44
  2. package/dist/commands/init.js +5 -5
  3. package/dist/commands/plugin/add.d.ts +0 -2
  4. package/dist/commands/plugin/add.js +6 -47
  5. package/dist/commands/plugin/pull.js +4 -6
  6. package/dist/commands/plugin/push.d.ts +1 -2
  7. package/dist/commands/plugin/push.js +18 -50
  8. package/dist/commands/project/pull.d.ts +8 -0
  9. package/dist/commands/project/pull.js +69 -0
  10. package/dist/commands/project/{sync.d.ts → push.d.ts} +1 -2
  11. package/dist/commands/project/{sync.js → push.js} +8 -44
  12. package/dist/commands/snippet/publish.js +1 -1
  13. package/dist/commands/status.js +6 -2
  14. package/dist/commands/telemetry/disable.d.ts +6 -0
  15. package/dist/commands/telemetry/disable.js +14 -0
  16. package/dist/commands/telemetry/enable.d.ts +6 -0
  17. package/dist/commands/telemetry/enable.js +14 -0
  18. package/dist/config/auth.manager.d.ts +4 -2
  19. package/dist/config/auth.manager.js +15 -5
  20. package/dist/config/project-config.manager.d.ts +7 -2
  21. package/dist/config/project-config.manager.js +39 -10
  22. package/dist/hooks/finally.js +17 -3
  23. package/dist/hooks/init.js +8 -16
  24. package/dist/lib/local-callback-server.d.ts +3 -3
  25. package/dist/lib/local-callback-server.js +14 -4
  26. package/dist/lib/open-browser.d.ts +1 -1
  27. package/dist/lib/open-browser.js +3 -10
  28. package/dist/lib/sentry.d.ts +1 -1
  29. package/dist/lib/sentry.js +17 -8
  30. package/dist/lib/wp-authorize-flow.d.ts +15 -3
  31. package/dist/lib/wp-authorize-flow.js +31 -24
  32. package/dist/lib/wp-client.d.ts +1 -1
  33. package/dist/lib/wp-client.js +1 -1
  34. package/dist/types/config.d.ts +1 -1
  35. package/dist/types/global-config.generated.d.ts +13 -3
  36. package/dist/types/plugin.d.ts +4 -5
  37. package/dist/types/project-config.generated.d.ts +1 -4
  38. package/dist/utils/plugins.d.ts +3 -7
  39. package/dist/utils/plugins.js +27 -13
  40. package/oclif.manifest.json +236 -169
  41. package/package.json +6 -2
  42. package/schemas/global-config.schema.json +19 -3
  43. package/schemas/project-config.schema.json +3 -4
@@ -3,8 +3,20 @@ export type AuthorizeResult = {
3
3
  userLogin: string;
4
4
  };
5
5
  /**
6
- * Runs WordPress's native "authorize application" flow: opens a local callback server,
7
- * sends the user to `<siteUrl>/wp-admin/authorize-application.php`, and resolves with the
8
- * generated Application Password once WordPress redirects back.
6
+ * Relays the authorization through the Loopress API so that WordPress's
7
+ * `success_url` / `reject_url` can be valid HTTPS URLs. The API receives the
8
+ * redirect from WordPress and forwards the credentials back to a local callback
9
+ * server via a form POST (keeping them out of the browser's address bar).
10
+ *
11
+ * Flow:
12
+ * 1. Start a local HTTP server on `127.0.0.1`.
13
+ * 2. Open the browser to `https://api.loopress.dev/auth/wp-authorize` with the local
14
+ * callback URL and the target WordPress site as query params.
15
+ * 3. That page redirects the user to WordPress's authorize-application.php,
16
+ * passing the API's callback endpoint as `success_url` (HTTPS).
17
+ * 4. After the user approves, WordPress redirects to the API callback.
18
+ * 5. The API callback returns an HTML page that POSTs a form with the
19
+ * Application Password to the local callback server.
20
+ * 6. The local server extracts the credentials and resolves the promise.
9
21
  */
10
22
  export declare function authorizeWithBrowser(siteUrl: string, log: (message: string) => void): Promise<AuthorizeResult>;
@@ -1,53 +1,60 @@
1
- import { randomBytes } from 'node:crypto';
2
1
  import { renderResultPage, waitForLocalCallback } from './local-callback-server.js';
3
- const APP_NAME = 'Loopress';
4
2
  /**
5
- * Runs WordPress's native "authorize application" flow: opens a local callback server,
6
- * sends the user to `<siteUrl>/wp-admin/authorize-application.php`, and resolves with the
7
- * generated Application Password once WordPress redirects back.
3
+ * Relays the authorization through the Loopress API so that WordPress's
4
+ * `success_url` / `reject_url` can be valid HTTPS URLs. The API receives the
5
+ * redirect from WordPress and forwards the credentials back to a local callback
6
+ * server via a form POST (keeping them out of the browser's address bar).
7
+ *
8
+ * Flow:
9
+ * 1. Start a local HTTP server on `127.0.0.1`.
10
+ * 2. Open the browser to `https://api.loopress.dev/auth/wp-authorize` with the local
11
+ * callback URL and the target WordPress site as query params.
12
+ * 3. That page redirects the user to WordPress's authorize-application.php,
13
+ * passing the API's callback endpoint as `success_url` (HTTPS).
14
+ * 4. After the user approves, WordPress redirects to the API callback.
15
+ * 5. The API callback returns an HTML page that POSTs a form with the
16
+ * Application Password to the local callback server.
17
+ * 6. The local server extracts the credentials and resolves the promise.
8
18
  */
9
19
  export function authorizeWithBrowser(siteUrl, log) {
10
- const state = randomBytes(32).toString('hex');
11
20
  return waitForLocalCallback({
12
21
  buildUrl(callbackBaseUrl) {
13
- const successUrl = `${callbackBaseUrl}/callback?state=${state}`;
14
- const rejectUrl = `${callbackBaseUrl}/reject?state=${state}`;
15
- return (`${siteUrl}/wp-admin/authorize-application.php?app_name=${encodeURIComponent(APP_NAME)}` +
16
- `&success_url=${encodeURIComponent(successUrl)}&reject_url=${encodeURIComponent(rejectUrl)}`);
22
+ const relayUrl = 'https://api.loopress.dev/auth/wp-authorize';
23
+ const params = new URLSearchParams({
24
+ callbackUrl: callbackBaseUrl,
25
+ wpUrl: siteUrl,
26
+ });
27
+ return `${relayUrl}?${params}`;
17
28
  },
18
- handleRequest(url, { rejectWithPage, resolveWithPage, respondBadRequest }) {
19
- if (url.searchParams.get('state') !== state) {
20
- respondBadRequest('Invalid or missing state');
29
+ handleRequest(url, { resolveWithPage, rejectWithPage, respondBadRequest, body }) {
30
+ if (url.searchParams.has('cancelled') || body.cancelled) {
31
+ rejectWithPage(REJECTED_PAGE, new Error('Authorization rejected in WordPress.'));
21
32
  return;
22
33
  }
23
- if (url.pathname === '/reject') {
24
- rejectWithPage(REJECTED_PAGE, new Error('Authorization rejected in WordPress. You can enter credentials manually instead.'));
25
- return;
26
- }
27
- const userLogin = url.searchParams.get('user_login');
28
- const password = url.searchParams.get('password');
29
- if (!userLogin || !password) {
30
- respondBadRequest('Missing user_login or password');
34
+ const password = body.password || url.searchParams.get('password') || '';
35
+ const userLogin = body.user_login || url.searchParams.get('user_login') || '';
36
+ if (!password || !userLogin) {
37
+ respondBadRequest('Missing password or user_login');
31
38
  return;
32
39
  }
33
40
  resolveWithPage(SUCCESS_PAGE, { password, userLogin });
34
41
  },
35
42
  log,
36
43
  openingMessage: 'Opening WordPress in your browser to authorize Loopress...',
37
- timeoutMessage: 'Authorization timed out after 5 minutes. You can enter credentials manually instead.',
44
+ timeoutMessage: 'Authorization timed out after 5 minutes.',
38
45
  });
39
46
  }
40
47
  const SUCCESS_PAGE = renderResultPage({
41
48
  background: '#f0fdf4',
42
49
  heading: 'Authorization successful!',
43
50
  headingColor: '#15803d',
44
- icon: '',
51
+ icon: '&#10003;',
45
52
  tabTitle: 'Authorized',
46
53
  });
47
54
  const REJECTED_PAGE = renderResultPage({
48
55
  background: '#fef2f2',
49
56
  heading: 'Authorization rejected',
50
57
  headingColor: '#b91c1c',
51
- icon: '',
58
+ icon: '&#10007;',
52
59
  tabTitle: 'Authorization rejected',
53
60
  });
@@ -1,7 +1,7 @@
1
1
  export declare const REQUEST_TIMEOUT_MS = 30000;
2
2
  /**
3
3
  * HTTP client for a WordPress site's REST API.
4
- * Paths are relative to `<site>/wp-json/`, e.g. `loopress/v1/plugins`.
4
+ * Paths are relative to `<site>/wp-json/`, e.g. `loopress/v1/snippets` or `wp/v2/plugins`.
5
5
  */
6
6
  export declare class WpClient {
7
7
  private readonly siteUrl;
@@ -2,7 +2,7 @@ import got from 'got';
2
2
  export const REQUEST_TIMEOUT_MS = 30_000;
3
3
  /**
4
4
  * HTTP client for a WordPress site's REST API.
5
- * Paths are relative to `<site>/wp-json/`, e.g. `loopress/v1/plugins`.
5
+ * Paths are relative to `<site>/wp-json/`, e.g. `loopress/v1/snippets` or `wp/v2/plugins`.
6
6
  */
7
7
  export class WpClient {
8
8
  siteUrl;
@@ -1 +1 @@
1
- export type { CurrentProjectPointer, EnvironmentConfig, LoopressGlobalConfiguration as LoopressConfig, ProjectConfig, } from './global-config.generated.js';
1
+ export type { CurrentProjectPointer, EnvironmentConfig, LoopressGlobalConfiguration as LoopressConfig, ProjectConfig, TelemetryConfig, } from './global-config.generated.js';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Global CLI state stored at ~/.loopress/config.json: known projects, their environments, and which one is currently active.
2
+ * Global CLI state stored at $XDG_CONFIG_HOME/loopress/config.json (or ~/.config/loopress/config.json): known projects, their environments, and which one is currently active.
3
3
  */
4
4
  export interface LoopressGlobalConfiguration {
5
5
  /**
@@ -12,6 +12,7 @@ export interface LoopressGlobalConfiguration {
12
12
  projects: {
13
13
  [k: string]: ProjectConfig;
14
14
  };
15
+ telemetry?: TelemetryConfig;
15
16
  }
16
17
  export interface CurrentProjectPointer {
17
18
  /**
@@ -29,7 +30,7 @@ export interface ProjectConfig {
29
30
  */
30
31
  addedAt: string;
31
32
  /**
32
- * Id of the matching project on the Loopress API, once synced via `lps project sync`.
33
+ * Id of the matching project on the Loopress API, once pushed via `lps project push`.
33
34
  */
34
35
  apiProjectId?: string;
35
36
  /**
@@ -49,7 +50,7 @@ export interface EnvironmentConfig {
49
50
  */
50
51
  addedAt: string;
51
52
  /**
52
- * Id of the matching environment on the Loopress API, once synced via `lps project sync`.
53
+ * Id of the matching environment on the Loopress API, once pushed via `lps project push`.
53
54
  */
54
55
  apiEnvironmentId?: string;
55
56
  /**
@@ -65,3 +66,12 @@ export interface EnvironmentConfig {
65
66
  */
66
67
  url: string;
67
68
  }
69
+ /**
70
+ * User preferences for error reporting, set via `lps telemetry disable`/`lps telemetry enable`.
71
+ */
72
+ export interface TelemetryConfig {
73
+ /**
74
+ * Whether error reporting to Sentry is disabled.
75
+ */
76
+ disabled: boolean;
77
+ }
@@ -5,11 +5,10 @@ export interface InstalledPlugin {
5
5
  slug: string;
6
6
  version: string;
7
7
  }
8
- export interface InstallResult {
9
- message: string;
8
+ export interface WpNativePlugin {
9
+ name: string;
10
+ plugin: string;
11
+ status: 'active' | 'inactive' | 'network-active';
10
12
  version: string;
11
13
  }
12
- export interface ActivateResult {
13
- message: string;
14
- }
15
14
  export type PluginManifest = Record<string, string>;
@@ -16,12 +16,9 @@ export interface LoopressProjectConfiguration {
16
16
  */
17
17
  projectId?: string;
18
18
  /**
19
- * Pinned plugin versions. Keys are WordPress.org plugin slugs, values are version constraints.
19
+ * WordPress.org plugins managed by Loopress. Keys are plugin slugs; values are written as "latest" since installs go through WordPress's native plugin API, which only installs the current version. Older files may still have a pinned version string here from before this was the case; it is read but no longer acted on.
20
20
  */
21
21
  plugins?: {
22
- /**
23
- * Version to pin. Use an exact version (e.g. "8.9.1") or "latest".
24
- */
25
22
  [k: string]: string;
26
23
  };
27
24
  }
@@ -1,16 +1,11 @@
1
- import { InstalledPlugin, PluginManifest } from '../types/plugin.js';
1
+ import { InstalledPlugin, PluginManifest, WpNativePlugin } from '../types/plugin.js';
2
2
  export interface PluginDiff {
3
- drifted: Array<{
4
- currentVersion: string;
5
- slug: string;
6
- targetVersion: string;
7
- }>;
8
3
  toActivate: Array<{
4
+ file: string;
9
5
  slug: string;
10
6
  }>;
11
7
  toInstall: Array<{
12
8
  slug: string;
13
- targetVersion: string;
14
9
  }>;
15
10
  upToDate: string[];
16
11
  }
@@ -24,4 +19,5 @@ export interface MergeResult {
24
19
  }>;
25
20
  }
26
21
  export declare function mergePluginManifest(existing: PluginManifest, incoming: PluginManifest): MergeResult;
22
+ export declare function parseInstalledPlugins(raw: WpNativePlugin[]): InstalledPlugin[];
27
23
  export declare function diffPlugins(manifest: PluginManifest, installed: InstalledPlugin[]): PluginDiff;
@@ -1,3 +1,7 @@
1
+ // Loopress must never manage itself: pulling it into loopress.json would make a later
2
+ // `plugin push` try to reinstall it from WordPress.org, where it doesn't exist, potentially
3
+ // clobbering the plugin's own directory in the process.
4
+ const LOOPRESS_PLUGIN_SLUG = 'loopress';
1
5
  export function mergePluginManifest(existing, incoming) {
2
6
  const merged = { ...existing, ...incoming };
3
7
  const added = Object.keys(incoming).filter((s) => !(s in existing));
@@ -6,29 +10,39 @@ export function mergePluginManifest(existing, incoming) {
6
10
  .map((s) => ({ from: existing[s], slug: s, to: incoming[s] }));
7
11
  return { added, merged, updated };
8
12
  }
13
+ // WordPress core identifies each plugin by a `<folder>/<file>` id (or a bare `<file>` for a
14
+ // single-file plugin) with the `.php` extension stripped; the WordPress.org slug is just the
15
+ // folder name (or the bare id itself for a single-file plugin).
16
+ function slugFromPluginFile(file) {
17
+ return file.split('/')[0];
18
+ }
19
+ export function parseInstalledPlugins(raw) {
20
+ return raw
21
+ .map((item) => ({
22
+ active: item.status !== 'inactive',
23
+ file: item.plugin,
24
+ name: item.name,
25
+ slug: slugFromPluginFile(item.plugin),
26
+ version: item.version,
27
+ }))
28
+ .filter((plugin) => plugin.slug !== LOOPRESS_PLUGIN_SLUG);
29
+ }
9
30
  export function diffPlugins(manifest, installed) {
10
31
  const installedMap = new Map(installed.map((p) => [p.slug, p]));
11
32
  const toInstall = [];
12
33
  const toActivate = [];
13
- const drifted = [];
14
34
  const upToDate = [];
15
- for (const [slug, targetVersion] of Object.entries(manifest)) {
35
+ for (const slug of Object.keys(manifest)) {
16
36
  const live = installedMap.get(slug);
17
37
  if (!live) {
18
- toInstall.push({ slug, targetVersion });
19
- continue;
38
+ toInstall.push({ slug });
20
39
  }
21
- if (live.version === targetVersion) {
22
- if (live.active) {
23
- upToDate.push(slug);
24
- }
25
- else {
26
- toActivate.push({ slug });
27
- }
40
+ else if (live.active) {
41
+ upToDate.push(slug);
28
42
  }
29
43
  else {
30
- drifted.push({ currentVersion: live.version, slug, targetVersion });
44
+ toActivate.push({ file: live.file, slug });
31
45
  }
32
46
  }
33
- return { drifted, toActivate, toInstall, upToDate };
47
+ return { toActivate, toInstall, upToDate };
34
48
  }