@loopress/cli 0.13.0 → 0.14.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 +62 -28
- package/dist/commands/init.js +5 -5
- package/dist/commands/plugin/add.d.ts +0 -2
- package/dist/commands/plugin/add.js +6 -47
- package/dist/commands/plugin/pull.js +4 -6
- package/dist/commands/plugin/push.d.ts +1 -2
- package/dist/commands/plugin/push.js +18 -50
- package/dist/commands/status.js +6 -2
- package/dist/commands/telemetry/disable.d.ts +6 -0
- package/dist/commands/telemetry/disable.js +14 -0
- package/dist/commands/telemetry/enable.d.ts +6 -0
- package/dist/commands/telemetry/enable.js +14 -0
- package/dist/config/auth.manager.d.ts +4 -2
- package/dist/config/auth.manager.js +15 -5
- package/dist/config/project-config.manager.d.ts +7 -2
- package/dist/config/project-config.manager.js +35 -6
- package/dist/hooks/finally.js +11 -2
- package/dist/hooks/init.js +8 -16
- package/dist/lib/local-callback-server.d.ts +3 -3
- package/dist/lib/local-callback-server.js +14 -4
- package/dist/lib/open-browser.d.ts +1 -1
- package/dist/lib/open-browser.js +3 -10
- package/dist/lib/sentry.d.ts +0 -1
- package/dist/lib/sentry.js +6 -8
- package/dist/lib/wp-authorize-flow.d.ts +15 -3
- package/dist/lib/wp-authorize-flow.js +31 -24
- package/dist/lib/wp-client.d.ts +1 -1
- package/dist/lib/wp-client.js +1 -1
- package/dist/types/config.d.ts +1 -1
- package/dist/types/global-config.generated.d.ts +11 -1
- package/dist/types/plugin.d.ts +4 -5
- package/dist/types/project-config.generated.d.ts +1 -4
- package/dist/utils/plugins.d.ts +3 -7
- package/dist/utils/plugins.js +27 -13
- package/oclif.manifest.json +133 -90
- package/package.json +6 -2
- package/schemas/global-config.schema.json +17 -1
- package/schemas/project-config.schema.json +3 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EnvironmentConfig, LoopressConfig, ProjectConfig } from '../types/config.js';
|
|
2
2
|
export declare class ProjectConfigManager {
|
|
3
|
-
private
|
|
4
|
-
constructor(
|
|
3
|
+
private configDir?;
|
|
4
|
+
constructor(configDir?: string | undefined);
|
|
5
5
|
createProjectId(name: string): string;
|
|
6
6
|
ensureConfigDir(): void;
|
|
7
7
|
findProjectByApiId(apiProjectId: string): null | (ProjectConfig & {
|
|
@@ -14,6 +14,7 @@ export declare class ProjectConfigManager {
|
|
|
14
14
|
});
|
|
15
15
|
getEnvironment(projectId: string, envName: string): EnvironmentConfig | null;
|
|
16
16
|
getProject(id: string): null | ProjectConfig;
|
|
17
|
+
isTelemetryDisabled(): boolean;
|
|
17
18
|
listEnvironments(projectId: string): Array<EnvironmentConfig & {
|
|
18
19
|
isCurrent: boolean;
|
|
19
20
|
}>;
|
|
@@ -24,15 +25,19 @@ export declare class ProjectConfigManager {
|
|
|
24
25
|
readConfig(): LoopressConfig;
|
|
25
26
|
removeEnvironment(projectId: string, envName: string): void;
|
|
26
27
|
removeProject(id: string): void;
|
|
28
|
+
setConfigDir(configDir: string): void;
|
|
27
29
|
setCurrent(projectId: string, envName: string): void;
|
|
28
30
|
setEnvironment(projectId: string, envName: string, env: EnvironmentConfig): void;
|
|
29
31
|
setEnvironmentApiId(projectId: string, envName: string, apiEnvironmentId: string): void;
|
|
30
32
|
setProject(id: string, project: ProjectConfig): void;
|
|
31
33
|
setProjectApiId(id: string, apiProjectId: string): void;
|
|
34
|
+
setTelemetryDisabled(disabled: boolean): void;
|
|
32
35
|
writeConfig(config: LoopressConfig): void;
|
|
33
36
|
private isProjectConfig;
|
|
37
|
+
private requireConfigDir;
|
|
34
38
|
private sanitizeConfig;
|
|
35
39
|
private sanitizeCurrentProject;
|
|
36
40
|
private sanitizeProjects;
|
|
41
|
+
private sanitizeTelemetry;
|
|
37
42
|
}
|
|
38
43
|
export declare const configManager: ProjectConfigManager;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import slugify from 'slugify';
|
|
5
4
|
import { readJsonFile, writeJsonFileAtomic } from './json-file.js';
|
|
6
5
|
export class ProjectConfigManager {
|
|
7
|
-
|
|
8
|
-
constructor(
|
|
9
|
-
this.
|
|
6
|
+
configDir;
|
|
7
|
+
constructor(configDir) {
|
|
8
|
+
this.configDir = configDir;
|
|
10
9
|
}
|
|
11
10
|
createProjectId(name) {
|
|
12
11
|
const config = this.readConfig();
|
|
@@ -20,7 +19,7 @@ export class ProjectConfigManager {
|
|
|
20
19
|
return id;
|
|
21
20
|
}
|
|
22
21
|
ensureConfigDir() {
|
|
23
|
-
const dir =
|
|
22
|
+
const dir = this.requireConfigDir();
|
|
24
23
|
if (!existsSync(dir)) {
|
|
25
24
|
mkdirSync(dir, { recursive: true });
|
|
26
25
|
}
|
|
@@ -38,7 +37,7 @@ export class ProjectConfigManager {
|
|
|
38
37
|
return null;
|
|
39
38
|
}
|
|
40
39
|
getConfigFilePath() {
|
|
41
|
-
return join(this.
|
|
40
|
+
return join(this.requireConfigDir(), 'config.json');
|
|
42
41
|
}
|
|
43
42
|
getCurrentEnv() {
|
|
44
43
|
const config = this.readConfig();
|
|
@@ -68,6 +67,9 @@ export class ProjectConfigManager {
|
|
|
68
67
|
const config = this.readConfig();
|
|
69
68
|
return config.projects[id] ?? null;
|
|
70
69
|
}
|
|
70
|
+
isTelemetryDisabled() {
|
|
71
|
+
return this.readConfig().telemetry?.disabled ?? false;
|
|
72
|
+
}
|
|
71
73
|
listEnvironments(projectId) {
|
|
72
74
|
const config = this.readConfig();
|
|
73
75
|
const project = config.projects[projectId];
|
|
@@ -116,6 +118,12 @@ export class ProjectConfigManager {
|
|
|
116
118
|
}
|
|
117
119
|
this.writeConfig(config);
|
|
118
120
|
}
|
|
121
|
+
// Repointed by the `init` hook to oclif's native configDir once the real CLI Config is
|
|
122
|
+
// available. The constructor default only serves contexts that bypass the oclif lifecycle
|
|
123
|
+
// (e.g. commands instantiated directly in unit tests).
|
|
124
|
+
setConfigDir(configDir) {
|
|
125
|
+
this.configDir = configDir;
|
|
126
|
+
}
|
|
119
127
|
setCurrent(projectId, envName) {
|
|
120
128
|
const config = this.readConfig();
|
|
121
129
|
if (!config.projects[projectId])
|
|
@@ -159,6 +167,11 @@ export class ProjectConfigManager {
|
|
|
159
167
|
project.apiProjectId = apiProjectId;
|
|
160
168
|
this.writeConfig(config);
|
|
161
169
|
}
|
|
170
|
+
setTelemetryDisabled(disabled) {
|
|
171
|
+
const config = this.readConfig();
|
|
172
|
+
config.telemetry = { disabled };
|
|
173
|
+
this.writeConfig(config);
|
|
174
|
+
}
|
|
162
175
|
writeConfig(config) {
|
|
163
176
|
writeJsonFileAtomic(this.getConfigFilePath(), config);
|
|
164
177
|
}
|
|
@@ -168,13 +181,23 @@ export class ProjectConfigManager {
|
|
|
168
181
|
const candidate = value;
|
|
169
182
|
return typeof candidate.name === 'string' && typeof candidate.environments === 'object' && candidate.environments !== null;
|
|
170
183
|
}
|
|
184
|
+
// Real CLI runs get this from the `init` hook (src/hooks/init.ts) before any command runs.
|
|
185
|
+
// Throwing when it's unset (rather than falling back to a hardcoded path) surfaces tests that
|
|
186
|
+
// forgot to configure the manager instead of silently touching some default location.
|
|
187
|
+
requireConfigDir() {
|
|
188
|
+
if (!this.configDir)
|
|
189
|
+
throw new Error('ProjectConfigManager used before setConfigDir() was called');
|
|
190
|
+
return this.configDir;
|
|
191
|
+
}
|
|
171
192
|
sanitizeConfig(value) {
|
|
172
193
|
if (typeof value !== 'object' || value === null)
|
|
173
194
|
return { currentProject: null, projects: {} };
|
|
174
195
|
const candidate = value;
|
|
196
|
+
const telemetry = this.sanitizeTelemetry(candidate.telemetry);
|
|
175
197
|
return {
|
|
176
198
|
currentProject: this.sanitizeCurrentProject(candidate.currentProject),
|
|
177
199
|
projects: this.sanitizeProjects(candidate.projects),
|
|
200
|
+
...(telemetry && { telemetry }),
|
|
178
201
|
};
|
|
179
202
|
}
|
|
180
203
|
sanitizeCurrentProject(value) {
|
|
@@ -193,5 +216,11 @@ export class ProjectConfigManager {
|
|
|
193
216
|
}
|
|
194
217
|
return projects;
|
|
195
218
|
}
|
|
219
|
+
sanitizeTelemetry(value) {
|
|
220
|
+
if (typeof value !== 'object' || value === null)
|
|
221
|
+
return undefined;
|
|
222
|
+
const candidate = value;
|
|
223
|
+
return typeof candidate.disabled === 'boolean' ? { disabled: candidate.disabled } : undefined;
|
|
224
|
+
}
|
|
196
225
|
}
|
|
197
226
|
export const configManager = new ProjectConfigManager();
|
package/dist/hooks/finally.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { isTelemetryDisabled, runtimeContext } from '../lib/sentry.js';
|
|
1
|
+
import { isTelemetryDisabled, resolveEnvironment, runtimeContext, SENTRY_DSN } from '../lib/sentry.js';
|
|
3
2
|
// oclif has no `command_error` hook (checked @oclif/core@4.11.11's hooks.d.ts). `finally`
|
|
4
3
|
// is the closest equivalent: it always runs at the end of the CLI lifecycle and carries
|
|
5
4
|
// the error, if any, so it's where we report crashes before the process exits.
|
|
5
|
+
//
|
|
6
|
+
// @sentry/node is imported dynamically here, only when there's actually an error to report.
|
|
7
|
+
// It's a heavy module (@opentelemetry deps, import-in-the-middle instrumentation), so loading
|
|
8
|
+
// it eagerly on every command would tax the common case where commands succeed.
|
|
6
9
|
const hook = async function (options) {
|
|
7
10
|
if (!options.error || isTelemetryDisabled())
|
|
8
11
|
return;
|
|
9
12
|
try {
|
|
13
|
+
const Sentry = await import('@sentry/node');
|
|
14
|
+
Sentry.init({
|
|
15
|
+
dsn: SENTRY_DSN,
|
|
16
|
+
environment: resolveEnvironment(),
|
|
17
|
+
release: this.config.version,
|
|
18
|
+
});
|
|
10
19
|
Sentry.captureException(options.error, {
|
|
11
20
|
contexts: { runtime: runtimeContext() },
|
|
12
21
|
extra: { argv: options.argv },
|
package/dist/hooks/init.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
dsn: SENTRY_DSN,
|
|
10
|
-
environment: resolveEnvironment(),
|
|
11
|
-
release: this.config.version,
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
catch (error) {
|
|
15
|
-
this.debug('Failed to initialize Sentry: %O', error);
|
|
16
|
-
}
|
|
1
|
+
import { authManager } from '../config/auth.manager.js';
|
|
2
|
+
import { configManager } from '../config/project-config.manager.js';
|
|
3
|
+
// configManager/authManager start unconfigured and throw if used before a directory is set.
|
|
4
|
+
// This hook runs before any command and points them at oclif's native, per-platform config/data
|
|
5
|
+
// directories as soon as the real CLI Config is available.
|
|
6
|
+
const hook = async function ({ config }) {
|
|
7
|
+
configManager.setConfigDir(config.configDir);
|
|
8
|
+
authManager.setDataDir(config.dataDir);
|
|
17
9
|
};
|
|
18
10
|
export default hook;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export type CallbackHelpers<T> = {
|
|
2
|
+
body: Record<string, string>;
|
|
2
3
|
rejectWithPage: (page: string, error: Error) => void;
|
|
3
4
|
resolveWithPage: (page: string, value: T) => void;
|
|
4
5
|
respondBadRequest: (message: string) => void;
|
|
5
6
|
};
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* timeout, and browser-opening boilerplate they'd otherwise duplicate.
|
|
8
|
+
* Sends the user to a URL in their browser and catches the resulting redirect on a short-lived
|
|
9
|
+
* local server; this factors out the server setup, timeout, and browser-opening boilerplate.
|
|
10
10
|
*/
|
|
11
11
|
export declare function waitForLocalCallback<T>(options: {
|
|
12
12
|
buildUrl: (callbackBaseUrl: string) => string;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { openBrowser } from './open-browser.js';
|
|
3
|
+
async function readBody(req) {
|
|
4
|
+
const chunks = [];
|
|
5
|
+
for await (const chunk of req)
|
|
6
|
+
chunks.push(chunk);
|
|
7
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
8
|
+
}
|
|
9
|
+
function parseFormData(body) {
|
|
10
|
+
return Object.fromEntries(new URLSearchParams(body));
|
|
11
|
+
}
|
|
3
12
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* timeout, and browser-opening boilerplate they'd otherwise duplicate.
|
|
13
|
+
* Sends the user to a URL in their browser and catches the resulting redirect on a short-lived
|
|
14
|
+
* local server; this factors out the server setup, timeout, and browser-opening boilerplate.
|
|
7
15
|
*/
|
|
8
16
|
export function waitForLocalCallback(options) {
|
|
9
17
|
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
|
|
@@ -15,9 +23,10 @@ export function waitForLocalCallback(options) {
|
|
|
15
23
|
server.close();
|
|
16
24
|
settle();
|
|
17
25
|
}
|
|
18
|
-
const server = createServer((req, res) => {
|
|
26
|
+
const server = createServer(async (req, res) => {
|
|
19
27
|
try {
|
|
20
28
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
29
|
+
const body = req.method === 'POST' ? parseFormData(await readBody(req)) : {};
|
|
21
30
|
options.handleRequest(url, {
|
|
22
31
|
rejectWithPage: (page, error) => finish(res, page, () => reject(error)),
|
|
23
32
|
resolveWithPage: (page, value) => finish(res, page, () => resolve(value)),
|
|
@@ -25,6 +34,7 @@ export function waitForLocalCallback(options) {
|
|
|
25
34
|
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
26
35
|
res.end(message);
|
|
27
36
|
},
|
|
37
|
+
body,
|
|
28
38
|
});
|
|
29
39
|
}
|
|
30
40
|
catch (error) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/** Opens `url` in the user's default browser, best-effort
|
|
1
|
+
/** Opens `url` in the user's default browser, best-effort. */
|
|
2
2
|
export declare function openBrowser(url: string): void;
|
package/dist/lib/open-browser.js
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
/** Opens `url` in the user's default browser, best-effort
|
|
1
|
+
import open from 'open';
|
|
2
|
+
/** Opens `url` in the user's default browser, best-effort. */
|
|
3
3
|
export function openBrowser(url) {
|
|
4
|
-
|
|
5
|
-
darwin: ['open', [url]],
|
|
6
|
-
linux: ['xdg-open', [url]],
|
|
7
|
-
win32: ['cmd', ['/c', 'start', '', url]],
|
|
8
|
-
};
|
|
9
|
-
const command = commands[process.platform];
|
|
10
|
-
if (command)
|
|
11
|
-
execFile(...command);
|
|
4
|
+
open(url).catch(() => { });
|
|
12
5
|
}
|
package/dist/lib/sentry.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export declare const SENTRY_DSN = "https://a08dd56bfffc2a45d5b8f665e4cb8b7d@o4511586904309760.ingest.de.sentry.io/4511673275973712";
|
|
2
|
-
export declare function consumeErrorReportingFlag(argv: string[]): void;
|
|
3
2
|
export declare function isTelemetryDisabled(): boolean;
|
|
4
3
|
export declare function resolveEnvironment(): string;
|
|
5
4
|
export declare function runtimeContext(): {
|
package/dist/lib/sentry.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { platform, release } from 'node:os';
|
|
2
|
+
import { configManager } from '../config/project-config.manager.js';
|
|
2
3
|
// DSNs are write-only and safe to embed in a distributed CLI, see https://docs.sentry.io/product/security/#can-i-make-my-sentry-dsn-private
|
|
3
4
|
export const SENTRY_DSN = 'https://a08dd56bfffc2a45d5b8f665e4cb8b7d@o4511586904309760.ingest.de.sentry.io/4511673275973712';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (index === -1)
|
|
7
|
-
return;
|
|
8
|
-
argv.splice(index, 1);
|
|
9
|
-
process.env.LOOPRESS_TELEMETRY_DISABLED = '1';
|
|
10
|
-
}
|
|
5
|
+
// The env var takes priority so CI/ephemeral environments can opt out for a single run
|
|
6
|
+
// without touching the persistent preference in the global config.json.
|
|
11
7
|
export function isTelemetryDisabled() {
|
|
12
|
-
|
|
8
|
+
if (process.env.LOOPRESS_TELEMETRY_DISABLED === '1')
|
|
9
|
+
return true;
|
|
10
|
+
return configManager.isTelemetryDisabled();
|
|
13
11
|
}
|
|
14
12
|
export function resolveEnvironment() {
|
|
15
13
|
if (process.env.SENTRY_ENVIRONMENT)
|
|
@@ -3,8 +3,20 @@ export type AuthorizeResult = {
|
|
|
3
3
|
userLogin: string;
|
|
4
4
|
};
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
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, {
|
|
19
|
-
if (url.searchParams.
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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.
|
|
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: '✓',
|
|
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: '✗',
|
|
52
59
|
tabTitle: 'Authorization rejected',
|
|
53
60
|
});
|
package/dist/lib/wp-client.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/wp-client.js
CHANGED
|
@@ -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;
|
package/dist/types/config.d.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -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
|
+
}
|
package/dist/types/plugin.d.ts
CHANGED
|
@@ -5,11 +5,10 @@ export interface InstalledPlugin {
|
|
|
5
5
|
slug: string;
|
|
6
6
|
version: string;
|
|
7
7
|
}
|
|
8
|
-
export interface
|
|
9
|
-
|
|
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
|
-
*
|
|
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
|
}
|
package/dist/utils/plugins.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/plugins.js
CHANGED
|
@@ -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
|
|
35
|
+
for (const slug of Object.keys(manifest)) {
|
|
16
36
|
const live = installedMap.get(slug);
|
|
17
37
|
if (!live) {
|
|
18
|
-
toInstall.push({ slug
|
|
19
|
-
continue;
|
|
38
|
+
toInstall.push({ slug });
|
|
20
39
|
}
|
|
21
|
-
if (live.
|
|
22
|
-
|
|
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
|
-
|
|
44
|
+
toActivate.push({ file: live.file, slug });
|
|
31
45
|
}
|
|
32
46
|
}
|
|
33
|
-
return {
|
|
47
|
+
return { toActivate, toInstall, upToDate };
|
|
34
48
|
}
|