@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.
- package/README.md +96 -44
- 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/project/pull.d.ts +8 -0
- package/dist/commands/project/pull.js +69 -0
- package/dist/commands/project/{sync.d.ts → push.d.ts} +1 -2
- package/dist/commands/project/{sync.js → push.js} +8 -44
- package/dist/commands/snippet/publish.js +1 -1
- 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 +39 -10
- package/dist/hooks/finally.js +17 -3
- 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 +1 -1
- package/dist/lib/sentry.js +17 -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 +13 -3
- 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 +236 -169
- package/package.json +6 -2
- package/schemas/global-config.schema.json +19 -3
- package/schemas/project-config.schema.json +3 -4
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { Listr } from 'listr2';
|
|
3
|
+
import { authManager } from '../../config/auth.manager.js';
|
|
4
|
+
import { configManager } from '../../config/project-config.manager.js';
|
|
5
|
+
import { ApiClient } from '../../lib/api-client.js';
|
|
6
|
+
export default class Pull extends Command {
|
|
7
|
+
static description = 'Pull projects and environments from your Loopress account that are not configured locally yet';
|
|
8
|
+
static examples = ['$ lps project pull'];
|
|
9
|
+
async run() {
|
|
10
|
+
await this.parse(Pull);
|
|
11
|
+
const token = authManager.getAuth()?.token;
|
|
12
|
+
if (!token) {
|
|
13
|
+
this.error('Not logged in. Run `lps login` first.');
|
|
14
|
+
}
|
|
15
|
+
const api = new ApiClient(token);
|
|
16
|
+
const apiProjects = await this.fetchApiProjects(api);
|
|
17
|
+
const linkedApiProjectIds = new Set(configManager
|
|
18
|
+
.listProjects()
|
|
19
|
+
.map((project) => project.apiProjectId)
|
|
20
|
+
.filter((id) => id !== undefined));
|
|
21
|
+
const newApiProjects = apiProjects.filter((apiProject) => !linkedApiProjectIds.has(apiProject.id));
|
|
22
|
+
let projectCount = 0;
|
|
23
|
+
let environmentCount = 0;
|
|
24
|
+
if (newApiProjects.length > 0) {
|
|
25
|
+
await new Listr(newApiProjects.map((apiProject) => ({
|
|
26
|
+
task: (_ctx, task) => {
|
|
27
|
+
this.pullProject(apiProject, task);
|
|
28
|
+
projectCount++;
|
|
29
|
+
environmentCount += apiProject.environments.length;
|
|
30
|
+
},
|
|
31
|
+
title: `Pull project "${apiProject.name}" from the API`,
|
|
32
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
33
|
+
}
|
|
34
|
+
this.log(`\n✓ Pulled ${projectCount} project${projectCount === 1 ? '' : 's'}, ${environmentCount} environment${environmentCount === 1 ? '' : 's'} from your Loopress account`);
|
|
35
|
+
}
|
|
36
|
+
async fetchApiProjects(api) {
|
|
37
|
+
try {
|
|
38
|
+
return await api.get('projects');
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
this.error(`Could not fetch projects from the API: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Reuse a local project already linked to this API project instead of always minting a new
|
|
45
|
+
// one: otherwise every pull that can't "claim" an existing link (e.g. after the local config
|
|
46
|
+
// was reset or desynced from the API) creates yet another duplicate entry.
|
|
47
|
+
pullProject(apiProject, task) {
|
|
48
|
+
const existing = configManager.findProjectByApiId(apiProject.id);
|
|
49
|
+
const environments = { ...existing?.environments };
|
|
50
|
+
for (const env of apiProject.environments) {
|
|
51
|
+
environments[env.name] = {
|
|
52
|
+
...environments[env.name],
|
|
53
|
+
addedAt: environments[env.name]?.addedAt ?? env.createdAt,
|
|
54
|
+
apiEnvironmentId: env.id,
|
|
55
|
+
name: env.name,
|
|
56
|
+
url: env.url,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
configManager.setProject(existing?.id ?? configManager.createProjectId(apiProject.name), {
|
|
60
|
+
addedAt: existing?.addedAt ?? apiProject.createdAt,
|
|
61
|
+
apiProjectId: apiProject.id,
|
|
62
|
+
environments,
|
|
63
|
+
name: apiProject.name,
|
|
64
|
+
});
|
|
65
|
+
const envCount = apiProject.environments.length;
|
|
66
|
+
if (task)
|
|
67
|
+
task.output = `Pulled with ${envCount} environment${envCount === 1 ? '' : 's'}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
export default class
|
|
2
|
+
export default class Push extends Command {
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
run(): Promise<void>;
|
|
@@ -8,6 +8,5 @@ export default class Sync extends Command {
|
|
|
8
8
|
private fetchApiProjects;
|
|
9
9
|
private planEnvironment;
|
|
10
10
|
private planProject;
|
|
11
|
-
private pullProject;
|
|
12
11
|
private pushCredentials;
|
|
13
12
|
}
|
|
@@ -5,11 +5,11 @@ import slugify from 'slugify';
|
|
|
5
5
|
import { authManager } from '../../config/auth.manager.js';
|
|
6
6
|
import { configManager } from '../../config/project-config.manager.js';
|
|
7
7
|
import { ApiClient } from '../../lib/api-client.js';
|
|
8
|
-
export default class
|
|
9
|
-
static description = '
|
|
10
|
-
static examples = ['$ lps project
|
|
8
|
+
export default class Push extends Command {
|
|
9
|
+
static description = 'Push locally configured projects, environments and credentials to your Loopress account';
|
|
10
|
+
static examples = ['$ lps project push'];
|
|
11
11
|
async run() {
|
|
12
|
-
await this.parse(
|
|
12
|
+
await this.parse(Push);
|
|
13
13
|
const token = authManager.getAuth()?.token;
|
|
14
14
|
if (!token) {
|
|
15
15
|
this.error('Not logged in. Run `lps login` first.');
|
|
@@ -77,22 +77,11 @@ export default class Sync extends Command {
|
|
|
77
77
|
await this.pushCredentials(api, plan.apiProjectId, envPlan.apiEnvironmentId, envPlan.env);
|
|
78
78
|
}
|
|
79
79
|
catch (error) {
|
|
80
|
-
this.warn(`Failed to
|
|
80
|
+
this.warn(`Failed to push "${plan.project.name}/${envPlan.env.name}": ${error.message}`);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
if (newApiProjects.length > 0) {
|
|
86
|
-
await new Listr(newApiProjects.map((apiProject) => ({
|
|
87
|
-
task: async (_ctx, task) => {
|
|
88
|
-
this.pullProject(apiProject, task);
|
|
89
|
-
projectCount++;
|
|
90
|
-
environmentCount += apiProject.environments.length;
|
|
91
|
-
},
|
|
92
|
-
title: `Pull project "${apiProject.name}" from the API`,
|
|
93
|
-
})), { concurrent: false, exitOnError: false }).run();
|
|
94
|
-
}
|
|
95
|
-
this.log(`\n✓ Synced ${projectCount} project${projectCount === 1 ? '' : 's'}, ${environmentCount} environment${environmentCount === 1 ? '' : 's'} with your Loopress account`);
|
|
84
|
+
this.log(`\n✓ Pushed ${projectCount} project${projectCount === 1 ? '' : 's'}, ${environmentCount} environment${environmentCount === 1 ? '' : 's'} to your Loopress account`);
|
|
96
85
|
}
|
|
97
86
|
async applyEnvironment(api, apiProjectId, envPlan, task) {
|
|
98
87
|
try {
|
|
@@ -111,7 +100,7 @@ export default class Sync extends Command {
|
|
|
111
100
|
task.output = envPlan.action === 'create' ? 'Created on the API' : 'Linked to the API';
|
|
112
101
|
}
|
|
113
102
|
catch (error) {
|
|
114
|
-
const message = `Failed to
|
|
103
|
+
const message = `Failed to push "${envPlan.env.name}": ${error.message}`;
|
|
115
104
|
if (task)
|
|
116
105
|
task.output = message;
|
|
117
106
|
throw error;
|
|
@@ -131,7 +120,7 @@ export default class Sync extends Command {
|
|
|
131
120
|
task.output = plan.action === 'create' ? 'Created on the API' : 'Linked to the API';
|
|
132
121
|
}
|
|
133
122
|
catch (error) {
|
|
134
|
-
const message = `Failed to
|
|
123
|
+
const message = `Failed to push project "${plan.project.name}": ${error.message}`;
|
|
135
124
|
if (task)
|
|
136
125
|
task.output = message;
|
|
137
126
|
throw error;
|
|
@@ -183,31 +172,6 @@ export default class Sync extends Command {
|
|
|
183
172
|
}
|
|
184
173
|
return { action: 'create', project };
|
|
185
174
|
}
|
|
186
|
-
pullProject(apiProject, task) {
|
|
187
|
-
// Reuse a local project already linked to this API project instead of always minting a
|
|
188
|
-
// new one: otherwise every sync run that can't "claim" an existing link (e.g. after the
|
|
189
|
-
// local config was reset or desynced from the API) creates yet another duplicate entry.
|
|
190
|
-
const existing = configManager.findProjectByApiId(apiProject.id);
|
|
191
|
-
const environments = { ...existing?.environments };
|
|
192
|
-
for (const env of apiProject.environments) {
|
|
193
|
-
environments[env.name] = {
|
|
194
|
-
...environments[env.name],
|
|
195
|
-
addedAt: environments[env.name]?.addedAt ?? env.createdAt,
|
|
196
|
-
apiEnvironmentId: env.id,
|
|
197
|
-
name: env.name,
|
|
198
|
-
url: env.url,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
configManager.setProject(existing?.id ?? configManager.createProjectId(apiProject.name), {
|
|
202
|
-
addedAt: existing?.addedAt ?? apiProject.createdAt,
|
|
203
|
-
apiProjectId: apiProject.id,
|
|
204
|
-
environments,
|
|
205
|
-
name: apiProject.name,
|
|
206
|
-
});
|
|
207
|
-
const envCount = apiProject.environments.length;
|
|
208
|
-
if (task)
|
|
209
|
-
task.output = `Pulled with ${envCount} environment${envCount === 1 ? '' : 's'}`;
|
|
210
|
-
}
|
|
211
175
|
async pushCredentials(api, apiProjectId, apiEnvironmentId, env) {
|
|
212
176
|
if (!env.token)
|
|
213
177
|
return;
|
|
@@ -32,7 +32,7 @@ export default class Publish extends Command {
|
|
|
32
32
|
this.error(`Project "${projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
|
|
33
33
|
}
|
|
34
34
|
if (!project.apiProjectId) {
|
|
35
|
-
this.error(`Project "${project.name}" is not linked to your Loopress account yet. Run \`lps project
|
|
35
|
+
this.error(`Project "${project.name}" is not linked to your Loopress account yet. Run \`lps project push\` first.`);
|
|
36
36
|
}
|
|
37
37
|
const path = args.path ?? join(localConfig.rootDir ?? '.', localConfig.snippetsDir ?? 'snippets');
|
|
38
38
|
this.log(`Publishing snippets from ${path}`);
|
package/dist/commands/status.js
CHANGED
|
@@ -10,9 +10,13 @@ export default class Status extends Command {
|
|
|
10
10
|
const localConfig = await readLocalConfig();
|
|
11
11
|
if (localConfig.projectId) {
|
|
12
12
|
this.reportPinnedProject(localConfig.projectId);
|
|
13
|
-
return;
|
|
14
13
|
}
|
|
15
|
-
|
|
14
|
+
else {
|
|
15
|
+
this.reportActiveProject();
|
|
16
|
+
}
|
|
17
|
+
this.log('');
|
|
18
|
+
this.log(`Config dir: ${this.config.configDir}`);
|
|
19
|
+
this.log(`Data dir: ${this.config.dataDir}`);
|
|
16
20
|
}
|
|
17
21
|
reportActiveProject() {
|
|
18
22
|
const env = configManager.getCurrentEnv();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { configManager } from '../../config/project-config.manager.js';
|
|
3
|
+
export default class TelemetryDisable extends Command {
|
|
4
|
+
static description = 'Disable error reporting to Sentry';
|
|
5
|
+
static examples = ['$ lps telemetry disable'];
|
|
6
|
+
async run() {
|
|
7
|
+
if (configManager.isTelemetryDisabled()) {
|
|
8
|
+
this.log('Error reporting is already disabled.');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
configManager.setTelemetryDisabled(true);
|
|
12
|
+
this.log('Error reporting disabled.');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { configManager } from '../../config/project-config.manager.js';
|
|
3
|
+
export default class TelemetryEnable extends Command {
|
|
4
|
+
static description = 'Enable error reporting to Sentry';
|
|
5
|
+
static examples = ['$ lps telemetry enable'];
|
|
6
|
+
async run() {
|
|
7
|
+
if (!configManager.isTelemetryDisabled()) {
|
|
8
|
+
this.log('Error reporting is already enabled.');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
configManager.setTelemetryDisabled(false);
|
|
12
|
+
this.log('Error reporting enabled.');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -4,11 +4,13 @@ export interface ConsoleAuth {
|
|
|
4
4
|
token: string;
|
|
5
5
|
}
|
|
6
6
|
export declare class AuthManager {
|
|
7
|
-
private
|
|
8
|
-
constructor(
|
|
7
|
+
private dataDir?;
|
|
8
|
+
constructor(dataDir?: string | undefined);
|
|
9
9
|
clearAuth(): void;
|
|
10
10
|
getAuth(): ConsoleAuth | null;
|
|
11
11
|
getAuthFilePath(): string;
|
|
12
12
|
setAuth(auth: ConsoleAuth): void;
|
|
13
|
+
setDataDir(dataDir: string): void;
|
|
14
|
+
private requireDataDir;
|
|
13
15
|
}
|
|
14
16
|
export declare const authManager: AuthManager;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import { readJsonFile, writeJsonFileAtomic } from './json-file.js';
|
|
5
4
|
export class AuthManager {
|
|
6
|
-
|
|
7
|
-
constructor(
|
|
8
|
-
this.
|
|
5
|
+
dataDir;
|
|
6
|
+
constructor(dataDir) {
|
|
7
|
+
this.dataDir = dataDir;
|
|
9
8
|
}
|
|
10
9
|
clearAuth() {
|
|
11
10
|
const filePath = this.getAuthFilePath();
|
|
@@ -16,10 +15,21 @@ export class AuthManager {
|
|
|
16
15
|
return readJsonFile(this.getAuthFilePath());
|
|
17
16
|
}
|
|
18
17
|
getAuthFilePath() {
|
|
19
|
-
return join(this.
|
|
18
|
+
return join(this.requireDataDir(), 'auth.json');
|
|
20
19
|
}
|
|
21
20
|
setAuth(auth) {
|
|
22
21
|
writeJsonFileAtomic(this.getAuthFilePath(), auth);
|
|
23
22
|
}
|
|
23
|
+
// Real CLI runs get this from the `init` hook (src/hooks/init.ts) before any command runs.
|
|
24
|
+
// Throwing when it's unset (rather than falling back to a hardcoded path) surfaces tests that
|
|
25
|
+
// forgot to configure the manager instead of silently touching some default location.
|
|
26
|
+
setDataDir(dataDir) {
|
|
27
|
+
this.dataDir = dataDir;
|
|
28
|
+
}
|
|
29
|
+
requireDataDir() {
|
|
30
|
+
if (!this.dataDir)
|
|
31
|
+
throw new Error('AuthManager used before setDataDir() was called');
|
|
32
|
+
return this.dataDir;
|
|
33
|
+
}
|
|
24
34
|
}
|
|
25
35
|
export const authManager = new AuthManager();
|
|
@@ -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,15 +19,15 @@ 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
|
}
|
|
27
26
|
}
|
|
28
|
-
// Looks up a local project already linked to a given API project
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
27
|
+
// Looks up a local project already linked to a given API project. Used by `project pull` to
|
|
28
|
+
// avoid minting a new local project every time it pulls an API project whose local link was
|
|
29
|
+
// lost (e.g. after a reset or partial config corruption), which otherwise accumulates
|
|
30
|
+
// duplicate entries.
|
|
32
31
|
findProjectByApiId(apiProjectId) {
|
|
33
32
|
const config = this.readConfig();
|
|
34
33
|
for (const [id, project] of Object.entries(config.projects)) {
|
|
@@ -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,15 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { isTelemetryDisabled, runtimeContext } from '../lib/sentry.js';
|
|
1
|
+
import { isTelemetryDisabled, redactArgv, 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
|
+
// Node defaults `server_name` to os.hostname(), which is often the user's real name
|
|
19
|
+
// (e.g. "jane-doe-macbook-pro"). sendDefaultPii covers IP addresses and similar, off by
|
|
20
|
+
// default but set explicitly since this reports from users' own machines.
|
|
21
|
+
sendDefaultPii: false,
|
|
22
|
+
serverName: 'loopress',
|
|
23
|
+
});
|
|
10
24
|
Sentry.captureException(options.error, {
|
|
11
25
|
contexts: { runtime: runtimeContext() },
|
|
12
|
-
extra: { argv: options.argv },
|
|
26
|
+
extra: { argv: redactArgv(options.argv) },
|
|
13
27
|
tags: { command: options.id },
|
|
14
28
|
});
|
|
15
29
|
await Sentry.flush(2000);
|
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,8 +1,8 @@
|
|
|
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(): {
|
|
6
5
|
node: string;
|
|
7
6
|
os: string;
|
|
8
7
|
};
|
|
8
|
+
export declare function redactArgv(argv: string[]): string[];
|
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)
|
|
@@ -22,3 +20,14 @@ export function runtimeContext() {
|
|
|
22
20
|
os: `${platform()} ${release()}`,
|
|
23
21
|
};
|
|
24
22
|
}
|
|
23
|
+
// Positional args and flag values can carry WordPress URLs, usernames, application passwords,
|
|
24
|
+
// or tokens (e.g. `lps login --token xxx`, `lps project config <url>`). Only flag names are
|
|
25
|
+
// safe to report, they help tell which code path crashed without leaking what was passed to it.
|
|
26
|
+
export function redactArgv(argv) {
|
|
27
|
+
return argv.map((arg) => {
|
|
28
|
+
if (!arg.startsWith('-'))
|
|
29
|
+
return '[REDACTED]';
|
|
30
|
+
const eqIndex = arg.indexOf('=');
|
|
31
|
+
return eqIndex === -1 ? arg : arg.slice(0, eqIndex);
|
|
32
|
+
});
|
|
33
|
+
}
|