@loopress/cli 0.12.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 +88 -29
- package/dist/commands/init.js +5 -5
- package/dist/commands/login.d.ts +0 -1
- package/dist/commands/login.js +21 -90
- 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/config.d.ts +2 -0
- package/dist/commands/project/config.js +45 -11
- package/dist/commands/snippet/publish.d.ts +10 -0
- package/dist/commands/snippet/publish.js +74 -0
- package/dist/commands/snippet/push.js +5 -70
- 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/load-snippets.d.ts +2 -0
- package/dist/lib/load-snippets.js +84 -0
- package/dist/lib/local-callback-server.d.ts +25 -0
- package/dist/lib/local-callback-server.js +103 -0
- package/dist/lib/open-browser.d.ts +2 -0
- package/dist/lib/open-browser.js +5 -0
- package/dist/lib/sentry.d.ts +0 -1
- package/dist/lib/sentry.js +6 -8
- package/dist/lib/wp-authorize-flow.d.ts +22 -0
- package/dist/lib/wp-authorize-flow.js +60 -0
- package/dist/lib/wp-client.d.ts +1 -1
- package/dist/lib/wp-client.js +1 -1
- package/dist/lib/wp-site-diagnostic.d.ts +13 -0
- package/dist/lib/wp-site-diagnostic.js +46 -0
- 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 +163 -90
- package/package.json +7 -2
- package/schemas/global-config.schema.json +17 -1
- package/schemas/project-config.schema.json +3 -4
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { confirm } from '@inquirer/prompts';
|
|
2
1
|
import { Listr } from 'listr2';
|
|
3
2
|
import { PushCommand } from '../../lib/push-command.js';
|
|
4
3
|
import { getComposerManagedSlugs, readComposerJson } from '../../utils/composer.js';
|
|
5
|
-
import { diffPlugins } from '../../utils/plugins.js';
|
|
4
|
+
import { diffPlugins, parseInstalledPlugins } from '../../utils/plugins.js';
|
|
6
5
|
export default class Push extends PushCommand {
|
|
7
6
|
static description = 'Push plugins to WordPress to match loopress.json';
|
|
8
7
|
static examples = ['$ lps plugin push', '$ lps plugin push --dry-run'];
|
|
@@ -25,45 +24,39 @@ export default class Push extends PushCommand {
|
|
|
25
24
|
this.log('Run `lps composer push` to deploy them.');
|
|
26
25
|
}
|
|
27
26
|
this.log(`Pushing plugins to ${url}`);
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
27
|
+
const raw = await this.wp.get('wp/v2/plugins');
|
|
28
|
+
const installed = parseInstalledPlugins(raw);
|
|
29
|
+
const { toActivate, toInstall } = diffPlugins(filteredManifest, installed);
|
|
30
|
+
if (toInstall.length === 0 && toActivate.length === 0) {
|
|
31
31
|
this.log('Everything is already in sync.');
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
if (toInstall.length > 0) {
|
|
35
35
|
this.log(`\nTo install (${toInstall.length}):`);
|
|
36
36
|
for (const a of toInstall)
|
|
37
|
-
this.log(` + ${a.slug}
|
|
37
|
+
this.log(` + ${a.slug}`);
|
|
38
38
|
}
|
|
39
39
|
if (toActivate.length > 0) {
|
|
40
40
|
this.log(`\nTo activate (${toActivate.length}):`);
|
|
41
41
|
for (const a of toActivate)
|
|
42
42
|
this.log(` ↑ ${a.slug}`);
|
|
43
43
|
}
|
|
44
|
-
if (drifted.length > 0) {
|
|
45
|
-
this.log(`\nVersion mismatch (${drifted.length}):`);
|
|
46
|
-
for (const a of drifted) {
|
|
47
|
-
this.log(` ~ ${a.slug}: site has ${a.currentVersion}, manifest wants ${a.targetVersion}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
44
|
if (this.dryRun)
|
|
51
45
|
return;
|
|
52
|
-
//
|
|
46
|
+
// Installing with `status: active` activates in the same call, so installs never need a
|
|
47
|
+
// separate activation step the way the old custom endpoint's two-step flow did.
|
|
53
48
|
if (toInstall.length > 0) {
|
|
54
49
|
await new Listr(toInstall.map((action) => ({
|
|
55
|
-
task: async (_ctx, task) => this.
|
|
56
|
-
title: `Install ${action.slug}
|
|
50
|
+
task: async (_ctx, task) => this.installPlugin(action.slug, task),
|
|
51
|
+
title: `Install ${action.slug}`,
|
|
57
52
|
})), { concurrent: false, exitOnError: false }).run();
|
|
58
53
|
}
|
|
59
|
-
// Activate installed-but-inactive plugins without prompting.
|
|
60
54
|
if (toActivate.length > 0) {
|
|
61
55
|
await new Listr(toActivate.map((action) => ({
|
|
62
|
-
task: async (_ctx, task) => this.activatePlugin(action.slug, task),
|
|
56
|
+
task: async (_ctx, task) => this.activatePlugin(action.file, action.slug, task),
|
|
63
57
|
title: `Activate ${action.slug}`,
|
|
64
58
|
})), { concurrent: false, exitOnError: false }).run();
|
|
65
59
|
}
|
|
66
|
-
await this.syncDrifted(drifted);
|
|
67
60
|
if (this.failedCount > 0) {
|
|
68
61
|
this.error(`${this.failedCount} plugin${this.failedCount === 1 ? '' : 's'} failed to install or activate.`);
|
|
69
62
|
}
|
|
@@ -75,18 +68,16 @@ export default class Push extends PushCommand {
|
|
|
75
68
|
// it falls back to plain logging. Rethrowing on failure (rather than swallowing) is what lets Listr
|
|
76
69
|
// mark the task as failed (red cross) instead of completed, even though `exitOnError: false` stops
|
|
77
70
|
// that failure from aborting sibling tasks in the same list.
|
|
78
|
-
async activatePlugin(slug, task) {
|
|
79
|
-
await this.performPluginAction('activate',
|
|
71
|
+
async activatePlugin(file, slug, task) {
|
|
72
|
+
await this.performPluginAction('activate', slug, () => this.wp.put(`wp/v2/plugins/${file}`, { status: 'active' }), task);
|
|
80
73
|
}
|
|
81
|
-
async
|
|
82
|
-
await this.performPluginAction('install',
|
|
83
|
-
await this.activatePlugin(slug, task);
|
|
74
|
+
async installPlugin(slug, task) {
|
|
75
|
+
await this.performPluginAction('install', slug, () => this.wp.post('wp/v2/plugins', { slug, status: 'active' }), task);
|
|
84
76
|
}
|
|
85
|
-
async performPluginAction(verb, request, task) {
|
|
86
|
-
const { body, endpoint, slug } = request;
|
|
77
|
+
async performPluginAction(verb, slug, request, task) {
|
|
87
78
|
try {
|
|
88
|
-
|
|
89
|
-
const message = `✓ ${
|
|
79
|
+
await request();
|
|
80
|
+
const message = `✓ ${slug} ${verb === 'install' ? 'installed and activated' : 'activated'}`;
|
|
90
81
|
if (task)
|
|
91
82
|
task.output = message;
|
|
92
83
|
else
|
|
@@ -102,27 +93,4 @@ export default class Push extends PushCommand {
|
|
|
102
93
|
throw error;
|
|
103
94
|
}
|
|
104
95
|
}
|
|
105
|
-
// Prompt per drifted plugin before syncing. Prompts run sequentially on plain stdout,
|
|
106
|
-
// before the Listr renderer takes over the terminal for the confirmed subset.
|
|
107
|
-
async syncDrifted(drifted) {
|
|
108
|
-
const confirmedDrift = [];
|
|
109
|
-
for (const action of drifted) {
|
|
110
|
-
const proceed = await confirm({
|
|
111
|
-
default: false,
|
|
112
|
-
message: `${action.slug} is at ${action.currentVersion} on the site but manifest wants ${action.targetVersion}. Sync to ${action.targetVersion}?`,
|
|
113
|
-
});
|
|
114
|
-
if (proceed) {
|
|
115
|
-
confirmedDrift.push(action);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
this.log(` Skipped ${action.slug}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (confirmedDrift.length === 0)
|
|
122
|
-
return;
|
|
123
|
-
await new Listr(confirmedDrift.map((action) => ({
|
|
124
|
-
task: async (_ctx, task) => this.installAndActivate(action.slug, action.targetVersion, task),
|
|
125
|
-
title: `Sync ${action.slug} to ${action.targetVersion}`,
|
|
126
|
-
})), { concurrent: false, exitOnError: false }).run();
|
|
127
|
-
}
|
|
128
96
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { confirm, input, password as passwordPrompt, select } from '@inquirer/prompts';
|
|
2
2
|
import { Command } from '@oclif/core';
|
|
3
3
|
import { configManager } from '../../config/project-config.manager.js';
|
|
4
|
+
import { authorizeWithBrowser } from '../../lib/wp-authorize-flow.js';
|
|
5
|
+
import { diagnoseWpSite } from '../../lib/wp-site-diagnostic.js';
|
|
4
6
|
const NEW_PROJECT = '__new__';
|
|
7
|
+
const AUTH_BROWSER = 'browser';
|
|
8
|
+
const AUTH_MANUAL = 'manual';
|
|
5
9
|
export default class Config extends Command {
|
|
6
|
-
static description = 'Add or update a WordPress project environment';
|
|
10
|
+
static description = 'Add or update a WordPress project environment. By default, authorizes via WordPress in your browser; manual username/Application Password entry is available as a fallback.';
|
|
7
11
|
static examples = ['$ lps project config'];
|
|
8
12
|
async run() {
|
|
9
13
|
await this.parse(Config);
|
|
@@ -34,7 +38,7 @@ export default class Config extends Command {
|
|
|
34
38
|
return;
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
|
-
const
|
|
41
|
+
const rawUrl = await input({
|
|
38
42
|
message: 'WordPress URL',
|
|
39
43
|
validate(value) {
|
|
40
44
|
try {
|
|
@@ -49,15 +53,8 @@ export default class Config extends Command {
|
|
|
49
53
|
}
|
|
50
54
|
},
|
|
51
55
|
});
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
validate: (value) => (value.trim().length > 0 ? true : 'Username cannot be empty'),
|
|
55
|
-
});
|
|
56
|
-
const appPassword = await passwordPrompt({
|
|
57
|
-
mask: '*',
|
|
58
|
-
message: 'Application password',
|
|
59
|
-
validate: (value) => (value.trim().length > 0 ? true : 'Application password cannot be empty'),
|
|
60
|
-
});
|
|
56
|
+
const url = rawUrl.replace(/\/+$/, '');
|
|
57
|
+
const { appPassword, user } = await this.resolveCredentials(url);
|
|
61
58
|
const token = `${user}:${appPassword}`;
|
|
62
59
|
const env = {
|
|
63
60
|
addedAt: new Date().toISOString(),
|
|
@@ -79,6 +76,43 @@ export default class Config extends Command {
|
|
|
79
76
|
this.log(`✓ "${projectName}/${envName}" configured`);
|
|
80
77
|
this.log('→ Run `lps project switch` to change the active project or environment');
|
|
81
78
|
}
|
|
79
|
+
async promptManualCredentials() {
|
|
80
|
+
const user = await input({
|
|
81
|
+
message: 'Username',
|
|
82
|
+
validate: (value) => (value.trim().length > 0 ? true : 'Username cannot be empty'),
|
|
83
|
+
});
|
|
84
|
+
const appPassword = await passwordPrompt({
|
|
85
|
+
mask: '*',
|
|
86
|
+
message: 'Application password',
|
|
87
|
+
validate: (value) => (value.trim().length > 0 ? true : 'Application password cannot be empty'),
|
|
88
|
+
});
|
|
89
|
+
return { appPassword, user };
|
|
90
|
+
}
|
|
91
|
+
async resolveCredentials(url) {
|
|
92
|
+
const authMode = await select({
|
|
93
|
+
choices: [
|
|
94
|
+
{ name: 'Authorize in my browser (recommended)', value: AUTH_BROWSER },
|
|
95
|
+
{ name: 'Enter credentials manually', value: AUTH_MANUAL },
|
|
96
|
+
],
|
|
97
|
+
message: 'How do you want to authenticate?',
|
|
98
|
+
});
|
|
99
|
+
if (authMode === AUTH_MANUAL) {
|
|
100
|
+
return this.promptManualCredentials();
|
|
101
|
+
}
|
|
102
|
+
const diagnostic = await diagnoseWpSite(url);
|
|
103
|
+
if (!diagnostic.ok) {
|
|
104
|
+
this.warn(`${diagnostic.reason}\nFalling back to manual credential entry.`);
|
|
105
|
+
return this.promptManualCredentials();
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const { password, userLogin } = await authorizeWithBrowser(url, (message) => this.log(message));
|
|
109
|
+
return { appPassword: password, user: userLogin };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.warn(`${error.message}\nFalling back to manual credential entry.`);
|
|
113
|
+
return this.promptManualCredentials();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
82
116
|
async resolveProject() {
|
|
83
117
|
const projects = configManager.listProjects();
|
|
84
118
|
if (projects.length > 0) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Publish extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
path: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
private toPayload;
|
|
10
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import slugify from 'slugify';
|
|
4
|
+
import { authManager } from '../../config/auth.manager.js';
|
|
5
|
+
import { configManager } from '../../config/project-config.manager.js';
|
|
6
|
+
import { ApiClient } from '../../lib/api-client.js';
|
|
7
|
+
import { loadSnippets } from '../../lib/load-snippets.js';
|
|
8
|
+
import { readLocalConfig } from '../../utils/loopress-config.js';
|
|
9
|
+
// Publishes to the Loopress api (not a WordPress site), so this does not extend
|
|
10
|
+
// `LoopressCommand`/`PushCommand`: those force an environment to be resolved, but a project
|
|
11
|
+
// publishing its snippets for sharing doesn't need one, a pure "library" project
|
|
12
|
+
// may have zero environments configured at all.
|
|
13
|
+
export default class Publish extends Command {
|
|
14
|
+
static args = {
|
|
15
|
+
path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
|
|
16
|
+
};
|
|
17
|
+
static description = 'Publish snippets to your Loopress account so they can be deployed to other projects. Does not touch any WordPress site.';
|
|
18
|
+
static examples = ['$ lps snippet publish', '$ lps snippet publish --path ./snippets'];
|
|
19
|
+
async run() {
|
|
20
|
+
const { args } = await this.parse(Publish);
|
|
21
|
+
const token = process.env.LOOPRESS_TOKEN ?? authManager.getAuth()?.token;
|
|
22
|
+
if (!token) {
|
|
23
|
+
this.error('Not logged in. Run `lps login` first.');
|
|
24
|
+
}
|
|
25
|
+
const localConfig = await readLocalConfig();
|
|
26
|
+
const projectId = localConfig.projectId ?? configManager.getCurrentProject()?.id;
|
|
27
|
+
if (!projectId) {
|
|
28
|
+
this.error('No project configured. Run `lps project config` first.');
|
|
29
|
+
}
|
|
30
|
+
const project = configManager.getProject(projectId);
|
|
31
|
+
if (!project) {
|
|
32
|
+
this.error(`Project "${projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
|
|
33
|
+
}
|
|
34
|
+
if (!project.apiProjectId) {
|
|
35
|
+
this.error(`Project "${project.name}" is not linked to your Loopress account yet. Run \`lps project sync\` first.`);
|
|
36
|
+
}
|
|
37
|
+
const path = args.path ?? join(localConfig.rootDir ?? '.', localConfig.snippetsDir ?? 'snippets');
|
|
38
|
+
this.log(`Publishing snippets from ${path}`);
|
|
39
|
+
let snippets;
|
|
40
|
+
try {
|
|
41
|
+
snippets = await loadSnippets(path);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
this.error(error.message);
|
|
45
|
+
}
|
|
46
|
+
const api = new ApiClient(token);
|
|
47
|
+
try {
|
|
48
|
+
await api.post(`projects/${project.apiProjectId}/snippets/publish`, {
|
|
49
|
+
snippets: snippets.map((snippet) => this.toPayload(snippet)),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
this.error(error.message);
|
|
54
|
+
}
|
|
55
|
+
this.log(`Published ${snippets.length} snippet${snippets.length === 1 ? '' : 's'} to your Loopress account.`);
|
|
56
|
+
}
|
|
57
|
+
toPayload(snippet) {
|
|
58
|
+
return {
|
|
59
|
+
active: snippet.active,
|
|
60
|
+
code: snippet.code,
|
|
61
|
+
insertMethod: snippet.insertMethod,
|
|
62
|
+
location: snippet.location,
|
|
63
|
+
name: snippet.name,
|
|
64
|
+
priority: snippet.priority,
|
|
65
|
+
shortcodeAttributes: snippet.shortcodeAttributes,
|
|
66
|
+
// Derived from the snippet's name rather than its on-disk filename: the same
|
|
67
|
+
// slugification `push`/`pull` already use for the canonical `<id>-<slug>` filename
|
|
68
|
+
// convention, so it stays stable even for a not-yet-pushed file with an arbitrary name.
|
|
69
|
+
slug: slugify(snippet.name, { lower: true, strict: true }),
|
|
70
|
+
tags: snippet.tags,
|
|
71
|
+
type: snippet.type,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { Listr } from 'listr2';
|
|
3
|
-
import {
|
|
3
|
+
import { readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { basename, dirname, extname, join } from 'node:path';
|
|
5
5
|
import slugify from 'slugify';
|
|
6
|
+
import { loadSnippets as loadSnippetsFromDisk } from '../../lib/load-snippets.js';
|
|
6
7
|
import { PushCommand } from '../../lib/push-command.js';
|
|
7
8
|
import { isNotFoundError } from '../../lib/wp-client.js';
|
|
8
|
-
import {
|
|
9
|
-
const TYPE_BY_EXTENSION = {
|
|
10
|
-
'.css': 'css',
|
|
11
|
-
'.html': 'html',
|
|
12
|
-
'.js': 'js',
|
|
13
|
-
'.php': 'php',
|
|
14
|
-
'.txt': 'text',
|
|
15
|
-
};
|
|
9
|
+
import { normalizeSnippet, SNIPPETS_ENDPOINT, stripPhpOpeningTag } from '../../utils/snippet-format.js';
|
|
16
10
|
export default class Push extends PushCommand {
|
|
17
11
|
static args = {
|
|
18
12
|
path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
|
|
@@ -78,71 +72,12 @@ export default class Push extends PushCommand {
|
|
|
78
72
|
await rm(oldMetaPath, { force: true });
|
|
79
73
|
}
|
|
80
74
|
async loadSnippets(path) {
|
|
81
|
-
const snippets = [];
|
|
82
|
-
let files;
|
|
83
75
|
try {
|
|
84
|
-
|
|
76
|
+
return await loadSnippetsFromDisk(path, (message) => this.warn(message));
|
|
85
77
|
}
|
|
86
78
|
catch (error) {
|
|
87
|
-
this.error(
|
|
79
|
+
this.error(error.message);
|
|
88
80
|
}
|
|
89
|
-
for (const file of files) {
|
|
90
|
-
const ext = extname(file);
|
|
91
|
-
if (!(ext in TYPE_BY_EXTENSION))
|
|
92
|
-
continue;
|
|
93
|
-
const filePath = join(path, file);
|
|
94
|
-
const metaPath = join(path, `${basename(file, ext)}.json`);
|
|
95
|
-
// One snippet's files are read in isolation: a corrupted or hand-broken sidecar
|
|
96
|
-
// (bad JSON, unreadable file, ...) must only skip that snippet, not abort the entire
|
|
97
|
-
// push for every other snippet in the directory.
|
|
98
|
-
try {
|
|
99
|
-
const content = await readFile(filePath, 'utf8');
|
|
100
|
-
let id;
|
|
101
|
-
let name;
|
|
102
|
-
let type;
|
|
103
|
-
let active = false;
|
|
104
|
-
let tags = [];
|
|
105
|
-
let location = null;
|
|
106
|
-
let insertMethod = null;
|
|
107
|
-
let priority = 10;
|
|
108
|
-
let shortcodeAttributes = [];
|
|
109
|
-
try {
|
|
110
|
-
const metaContent = await readFile(metaPath, 'utf8');
|
|
111
|
-
const meta = JSON.parse(metaContent);
|
|
112
|
-
id = meta.id ? Number(meta.id) : undefined;
|
|
113
|
-
name = meta.name ? String(meta.name) : undefined;
|
|
114
|
-
type = parseType(meta.type) ?? undefined;
|
|
115
|
-
active = Boolean(meta.active);
|
|
116
|
-
tags = Array.isArray(meta.tags) ? meta.tags.map(String) : [];
|
|
117
|
-
location = parseLocation(meta.location);
|
|
118
|
-
insertMethod = parseInsertMethod(meta.insertMethod);
|
|
119
|
-
priority = meta.priority === undefined ? 10 : Number(meta.priority);
|
|
120
|
-
shortcodeAttributes = Array.isArray(meta.shortcodeAttributes) ? meta.shortcodeAttributes.map(String) : [];
|
|
121
|
-
}
|
|
122
|
-
catch (error) {
|
|
123
|
-
if (error.code !== 'ENOENT')
|
|
124
|
-
throw error;
|
|
125
|
-
}
|
|
126
|
-
const resolvedType = type ?? (ext in TYPE_BY_EXTENSION ? TYPE_BY_EXTENSION[ext] : 'php');
|
|
127
|
-
snippets.push({
|
|
128
|
-
active,
|
|
129
|
-
code: content,
|
|
130
|
-
id,
|
|
131
|
-
insertMethod: insertMethod ?? 'auto',
|
|
132
|
-
location: location ?? defaultLocationForType(resolvedType),
|
|
133
|
-
name: name ?? basename(file, ext),
|
|
134
|
-
path: filePath,
|
|
135
|
-
priority,
|
|
136
|
-
shortcodeAttributes,
|
|
137
|
-
tags,
|
|
138
|
-
type: resolvedType,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
this.warn(`Skipping "${metaPath}": ${error.message}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return snippets;
|
|
146
81
|
}
|
|
147
82
|
// Throwing on failure (rather than returning a boolean) is what lets Listr mark the task as
|
|
148
83
|
// failed (red cross) instead of completed; `exitOnError: false` on the task list still lets
|
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,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();
|