@loopress/cli 0.7.0 → 0.9.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 +44 -75
- package/dist/commands/composer/pull.d.ts +0 -3
- package/dist/commands/composer/pull.js +4 -15
- package/dist/commands/composer/push.d.ts +0 -3
- package/dist/commands/composer/push.js +7 -17
- package/dist/commands/login.js +1 -1
- package/dist/commands/logout.js +1 -1
- package/dist/commands/plugin/add.d.ts +0 -3
- package/dist/commands/plugin/add.js +9 -10
- package/dist/commands/plugin/pull.d.ts +0 -3
- package/dist/commands/plugin/pull.js +7 -19
- package/dist/commands/plugin/push.d.ts +4 -3
- package/dist/commands/plugin/push.js +69 -63
- package/dist/commands/sentry-test.d.ts +6 -0
- package/dist/commands/sentry-test.js +8 -0
- package/dist/commands/snippet/list.d.ts +1 -4
- package/dist/commands/snippet/list.js +24 -44
- package/dist/commands/snippet/pull.d.ts +1 -4
- package/dist/commands/snippet/pull.js +40 -56
- package/dist/commands/snippet/push.d.ts +2 -4
- package/dist/commands/snippet/push.js +90 -77
- package/dist/config/auth.manager.d.ts +0 -2
- package/dist/config/auth.manager.js +5 -25
- package/dist/config/json-file.d.ts +2 -0
- package/dist/config/json-file.js +21 -0
- package/dist/config/project-config.manager.d.ts +1 -3
- package/dist/config/project-config.manager.js +7 -23
- package/dist/hooks/finally.d.ts +3 -0
- package/dist/hooks/finally.js +21 -0
- package/dist/hooks/init.d.ts +3 -0
- package/dist/hooks/init.js +18 -0
- package/dist/lib/base.d.ts +13 -8
- package/dist/lib/base.js +45 -43
- package/dist/lib/push-command.d.ts +0 -1
- package/dist/lib/push-command.js +0 -1
- package/dist/lib/sentry.d.ts +8 -0
- package/dist/lib/sentry.js +24 -0
- package/dist/lib/wp-client.d.ts +15 -0
- package/dist/lib/wp-client.js +53 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/global-config.generated.d.ts +59 -0
- package/dist/types/global-config.generated.js +2 -0
- package/dist/types/project-config.generated.d.ts +31 -0
- package/dist/types/project-config.generated.js +2 -0
- package/dist/types/snippet.d.ts +7 -1
- package/dist/types/snippet.generated.d.ts +46 -0
- package/dist/types/snippet.generated.js +2 -0
- package/dist/utils/loopress-config.d.ts +2 -7
- package/dist/utils/loopress-config.js +5 -2
- package/dist/utils/snippet-plugin-flag.d.ts +3 -0
- package/dist/utils/snippet-plugin-flag.js +8 -0
- package/dist/utils/snippet-plugin.d.ts +23 -2
- package/dist/utils/snippet-plugin.js +168 -13
- package/oclif.manifest.json +66 -200
- package/package.json +19 -5
- package/dist/config/types.d.ts +0 -19
- package/dist/types/menu.d.ts +0 -7
- package/dist/types/menu.js +0 -1
- /package/dist/{config/types.js → types/config.js} +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Args
|
|
2
|
-
import
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { Listr } from 'listr2';
|
|
3
|
+
import { readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { basename, dirname, extname, join } from 'node:path';
|
|
4
5
|
import slugify from 'slugify';
|
|
5
6
|
import { PushCommand } from '../../lib/push-command.js';
|
|
6
|
-
import {
|
|
7
|
+
import { snippetPluginFlag } from '../../utils/snippet-plugin-flag.js';
|
|
8
|
+
import { defaultLocationForType, getSnippetPlugin, parseInsertMethod, parseLocation, parseType, } from '../../utils/snippet-plugin.js';
|
|
7
9
|
const TYPE_BY_EXTENSION = {
|
|
8
10
|
'.css': 'css',
|
|
9
11
|
'.html': 'html',
|
|
@@ -16,53 +18,38 @@ export default class Push extends PushCommand {
|
|
|
16
18
|
path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
|
|
17
19
|
};
|
|
18
20
|
static description = 'Push snippets to WordPress. Local snippet files created or updated remotely are renamed on disk to the `<id>-<slug>` convention.';
|
|
19
|
-
static examples = [
|
|
20
|
-
'$ lps snippet push',
|
|
21
|
-
'$ lps snippet push --url http://example.com',
|
|
22
|
-
'$ lps snippet push --path ./snippets',
|
|
23
|
-
'$ lps snippet push --plugin wpcode',
|
|
24
|
-
];
|
|
21
|
+
static examples = ['$ lps snippet push', '$ lps snippet push --path ./snippets', '$ lps snippet push --plugin wpcode'];
|
|
25
22
|
static flags = {
|
|
26
|
-
...PushCommand.
|
|
27
|
-
|
|
28
|
-
plugin: Flags.string({
|
|
29
|
-
char: 'p',
|
|
30
|
-
description: 'WordPress snippet plugin to target (overrides loopress.json)',
|
|
31
|
-
options: ['code-snippets', 'wpcode'],
|
|
32
|
-
}),
|
|
23
|
+
...PushCommand.dryRunFlag,
|
|
24
|
+
...snippetPluginFlag,
|
|
33
25
|
};
|
|
26
|
+
failedCount = 0;
|
|
34
27
|
async run() {
|
|
35
28
|
const { args, flags } = await this.parse(Push);
|
|
36
|
-
const dryRun = flags['dry-run'];
|
|
37
|
-
const { plugin } = flags;
|
|
38
|
-
this.dryRun = dryRun;
|
|
39
29
|
const { url } = this.siteConfig;
|
|
40
|
-
const path =
|
|
41
|
-
const resolvedPlugin =
|
|
42
|
-
this.log(
|
|
43
|
-
this.log(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
await this.recordSuccess();
|
|
55
|
-
this.log('🎉 All snippets pushed successfully!');
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
this.error(error.message);
|
|
30
|
+
const path = this.resolveSnippetsPath(args.path);
|
|
31
|
+
const resolvedPlugin = this.resolveSnippetPlugin(flags.plugin);
|
|
32
|
+
this.log(`Pushing snippets to ${url} via ${resolvedPlugin}`);
|
|
33
|
+
this.log(`Snippets path: ${path}`);
|
|
34
|
+
const snippets = await this.loadSnippets(path);
|
|
35
|
+
this.log(`Found ${snippets.length} snippet${snippets.length === 1 ? '' : 's'} to push`);
|
|
36
|
+
const adapter = getSnippetPlugin(resolvedPlugin);
|
|
37
|
+
await new Listr(snippets.map((snippet) => ({
|
|
38
|
+
task: async (_ctx, task) => this.pushSnippet(snippet, adapter, task),
|
|
39
|
+
title: `Push ${snippet.name}`,
|
|
40
|
+
})), { concurrent: false, exitOnError: false }).run();
|
|
41
|
+
if (this.failedCount > 0) {
|
|
42
|
+
this.error(`${this.failedCount} snippet${this.failedCount === 1 ? '' : 's'} failed to push.`);
|
|
59
43
|
}
|
|
44
|
+
if (this.dryRun)
|
|
45
|
+
return;
|
|
46
|
+
await this.recordSuccess();
|
|
47
|
+
this.log('All snippets pushed.');
|
|
60
48
|
}
|
|
61
49
|
// Renames the local file pair to the `<id>-<slug>` convention used by `snippet pull` whenever
|
|
62
50
|
// it doesn't already match (e.g. a hand-created `demo.php` with no id, or a stale slug after a rename).
|
|
63
51
|
// This is a side effect of `push`: local files on disk are renamed, not just the remote snippet.
|
|
64
52
|
async ensureCanonicalFilename(snippet, id, name) {
|
|
65
|
-
const fs = await import('node:fs/promises');
|
|
66
53
|
const dir = dirname(snippet.path);
|
|
67
54
|
const ext = extname(snippet.path);
|
|
68
55
|
const currentBase = basename(snippet.path, ext);
|
|
@@ -70,7 +57,7 @@ export default class Push extends PushCommand {
|
|
|
70
57
|
const oldMetaPath = join(dir, `${currentBase}.json`);
|
|
71
58
|
let meta = {};
|
|
72
59
|
try {
|
|
73
|
-
const existing = await
|
|
60
|
+
const existing = await readFile(oldMetaPath, 'utf8');
|
|
74
61
|
meta = JSON.parse(existing);
|
|
75
62
|
}
|
|
76
63
|
catch (error) {
|
|
@@ -79,84 +66,110 @@ export default class Push extends PushCommand {
|
|
|
79
66
|
}
|
|
80
67
|
meta.id = id;
|
|
81
68
|
meta.name = name;
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
meta.type = snippet.type;
|
|
70
|
+
// Persist the id against the *current* file pairing before renaming anything, so a
|
|
71
|
+
// crash between the rename and the sidecar write still leaves a valid `<name>.<ext>` /
|
|
72
|
+
// `<name>.json` pair with the id on disk, and a retried `snippet push` won't re-create
|
|
73
|
+
// this snippet as a duplicate because it looks unlinked.
|
|
74
|
+
await writeFile(oldMetaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
75
|
+
if (currentBase === canonicalBase)
|
|
84
76
|
return;
|
|
85
|
-
}
|
|
86
77
|
const newPath = join(dir, `${canonicalBase}${ext}`);
|
|
87
78
|
const newMetaPath = join(dir, `${canonicalBase}.json`);
|
|
88
|
-
await
|
|
89
|
-
await
|
|
90
|
-
|
|
91
|
-
await fs.rm(oldMetaPath, { force: true });
|
|
92
|
-
this.log(`📁 Renamed: ${snippet.path} → ${newPath}`);
|
|
79
|
+
await rename(snippet.path, newPath);
|
|
80
|
+
await writeFile(newMetaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
81
|
+
await rm(oldMetaPath, { force: true });
|
|
93
82
|
}
|
|
94
83
|
async loadSnippets(path) {
|
|
95
|
-
const fs = await import('node:fs/promises');
|
|
96
84
|
const snippets = [];
|
|
97
|
-
const SNIPPET_EXTENSIONS = new Set(Object.keys(TYPE_BY_EXTENSION));
|
|
98
85
|
try {
|
|
99
|
-
const files = await
|
|
86
|
+
const files = await readdir(path);
|
|
100
87
|
for (const file of files) {
|
|
101
|
-
const ext =
|
|
102
|
-
if (!
|
|
88
|
+
const ext = extname(file);
|
|
89
|
+
if (!(ext in TYPE_BY_EXTENSION))
|
|
103
90
|
continue;
|
|
104
|
-
const filePath =
|
|
105
|
-
const metaPath =
|
|
106
|
-
const content = await
|
|
91
|
+
const filePath = join(path, file);
|
|
92
|
+
const metaPath = join(path, `${basename(file, ext)}.json`);
|
|
93
|
+
const content = await readFile(filePath, 'utf8');
|
|
107
94
|
let id;
|
|
108
95
|
let name;
|
|
109
96
|
let type;
|
|
97
|
+
let active = false;
|
|
98
|
+
let tags = [];
|
|
99
|
+
let location = null;
|
|
100
|
+
let insertMethod = null;
|
|
101
|
+
let priority = 10;
|
|
102
|
+
let shortcodeAttributes = [];
|
|
110
103
|
try {
|
|
111
|
-
const metaContent = await
|
|
104
|
+
const metaContent = await readFile(metaPath, 'utf8');
|
|
112
105
|
const meta = JSON.parse(metaContent);
|
|
113
106
|
id = meta.id ? Number(meta.id) : undefined;
|
|
114
107
|
name = meta.name ? String(meta.name) : undefined;
|
|
115
108
|
type = parseType(meta.type) ?? undefined;
|
|
109
|
+
active = Boolean(meta.active);
|
|
110
|
+
tags = Array.isArray(meta.tags) ? meta.tags.map(String) : [];
|
|
111
|
+
location = parseLocation(meta.location);
|
|
112
|
+
insertMethod = parseInsertMethod(meta.insertMethod);
|
|
113
|
+
priority = meta.priority === undefined ? 10 : Number(meta.priority);
|
|
114
|
+
shortcodeAttributes = Array.isArray(meta.shortcodeAttributes) ? meta.shortcodeAttributes.map(String) : [];
|
|
116
115
|
}
|
|
117
116
|
catch (error) {
|
|
118
117
|
if (error.code !== 'ENOENT')
|
|
119
118
|
throw error;
|
|
120
119
|
}
|
|
120
|
+
const resolvedType = type ?? (ext in TYPE_BY_EXTENSION ? TYPE_BY_EXTENSION[ext] : 'php');
|
|
121
121
|
snippets.push({
|
|
122
|
+
active,
|
|
122
123
|
code: content,
|
|
123
124
|
id,
|
|
124
|
-
|
|
125
|
+
insertMethod: insertMethod ?? 'auto',
|
|
126
|
+
location: location ?? defaultLocationForType(resolvedType),
|
|
127
|
+
name: name ?? basename(file, ext),
|
|
125
128
|
path: filePath,
|
|
126
|
-
|
|
129
|
+
priority,
|
|
130
|
+
shortcodeAttributes,
|
|
131
|
+
tags,
|
|
132
|
+
type: resolvedType,
|
|
127
133
|
});
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
catch (error) {
|
|
131
|
-
this.error(
|
|
137
|
+
this.error(`Error loading snippets: ${error.message}`);
|
|
132
138
|
}
|
|
133
139
|
return snippets;
|
|
134
140
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
// Throwing on failure (rather than returning a boolean) is what lets Listr mark the task as
|
|
142
|
+
// failed (red cross) instead of completed; `exitOnError: false` on the task list still lets
|
|
143
|
+
// sibling snippets push regardless.
|
|
144
|
+
async pushSnippet(snippet, adapter, task) {
|
|
145
|
+
if (this.dryRun) {
|
|
146
|
+
if (task)
|
|
147
|
+
task.output = `[dry-run] Would push: ${snippet.name}`;
|
|
140
148
|
return;
|
|
141
149
|
}
|
|
150
|
+
const endpointPath = adapter.endpointPath();
|
|
142
151
|
try {
|
|
143
|
-
const
|
|
144
|
-
const payload = adapter.toPayload(snippet.name, snippet.code, snippet.path, snippet.type);
|
|
152
|
+
const payload = adapter.toPayload(snippet);
|
|
145
153
|
if (snippet.id) {
|
|
146
|
-
this.
|
|
147
|
-
await got.put(`${endpoint}/${snippet.id}`, { headers, json: payload });
|
|
148
|
-
this.log(`✅ Updated: ${snippet.name}`);
|
|
154
|
+
await this.wp.put(`${endpointPath}/${snippet.id}`, payload);
|
|
149
155
|
await this.ensureCanonicalFilename(snippet, snippet.id, snippet.name);
|
|
150
|
-
return;
|
|
151
156
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
else {
|
|
158
|
+
const response = await this.wp.post(endpointPath, payload);
|
|
159
|
+
const created = adapter.fromRemote(response);
|
|
160
|
+
await this.ensureCanonicalFilename(snippet, created.id, created.name);
|
|
161
|
+
}
|
|
162
|
+
if (task)
|
|
163
|
+
task.output = `Pushed: ${snippet.name}`;
|
|
157
164
|
}
|
|
158
165
|
catch (error) {
|
|
159
|
-
|
|
166
|
+
const message = `Failed to push ${snippet.name}: ${error.message}`;
|
|
167
|
+
if (task)
|
|
168
|
+
task.output = message;
|
|
169
|
+
else
|
|
170
|
+
this.warn(` ${message}`);
|
|
171
|
+
this.failedCount++;
|
|
172
|
+
throw error;
|
|
160
173
|
}
|
|
161
174
|
}
|
|
162
175
|
}
|
|
@@ -5,9 +5,7 @@ export interface ConsoleAuth {
|
|
|
5
5
|
}
|
|
6
6
|
export declare class AuthManager {
|
|
7
7
|
private readonly homeDir;
|
|
8
|
-
private static instance;
|
|
9
8
|
constructor(homeDir?: string);
|
|
10
|
-
static getInstance(): AuthManager;
|
|
11
9
|
clearAuth(): void;
|
|
12
10
|
getAuth(): ConsoleAuth | null;
|
|
13
11
|
getAuthFilePath(): string;
|
|
@@ -1,45 +1,25 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { readJsonFile, writeJsonFileAtomic } from './json-file.js';
|
|
4
5
|
export class AuthManager {
|
|
5
6
|
homeDir;
|
|
6
|
-
static instance;
|
|
7
7
|
constructor(homeDir = homedir()) {
|
|
8
8
|
this.homeDir = homeDir;
|
|
9
9
|
}
|
|
10
|
-
static getInstance() {
|
|
11
|
-
if (!AuthManager.instance) {
|
|
12
|
-
AuthManager.instance = new AuthManager();
|
|
13
|
-
}
|
|
14
|
-
return AuthManager.instance;
|
|
15
|
-
}
|
|
16
10
|
clearAuth() {
|
|
17
11
|
const filePath = this.getAuthFilePath();
|
|
18
12
|
if (existsSync(filePath))
|
|
19
13
|
unlinkSync(filePath);
|
|
20
14
|
}
|
|
21
15
|
getAuth() {
|
|
22
|
-
|
|
23
|
-
if (!existsSync(filePath))
|
|
24
|
-
return null;
|
|
25
|
-
try {
|
|
26
|
-
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
16
|
+
return readJsonFile(this.getAuthFilePath());
|
|
31
17
|
}
|
|
32
18
|
getAuthFilePath() {
|
|
33
19
|
return join(this.homeDir, '.loopress', 'auth.json');
|
|
34
20
|
}
|
|
35
21
|
setAuth(auth) {
|
|
36
|
-
|
|
37
|
-
if (!existsSync(dir))
|
|
38
|
-
mkdirSync(dir, { recursive: true });
|
|
39
|
-
const filePath = this.getAuthFilePath();
|
|
40
|
-
const tmpPath = `${filePath}.tmp`;
|
|
41
|
-
writeFileSync(tmpPath, JSON.stringify(auth, null, 2));
|
|
42
|
-
renameSync(tmpPath, filePath);
|
|
22
|
+
writeJsonFileAtomic(this.getAuthFilePath(), auth);
|
|
43
23
|
}
|
|
44
24
|
}
|
|
45
|
-
export const authManager = AuthManager
|
|
25
|
+
export const authManager = new AuthManager();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import writeFileAtomic from 'write-file-atomic';
|
|
4
|
+
// Missing file or invalid JSON are treated as "no data" (returns null). Any other
|
|
5
|
+
// read failure (permissions, EISDIR, ...) propagates instead of being swallowed.
|
|
6
|
+
export function readJsonFile(filePath) {
|
|
7
|
+
if (!existsSync(filePath))
|
|
8
|
+
return null;
|
|
9
|
+
const content = readFileSync(filePath, 'utf8');
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(content);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Mode 0o600 (owner read/write only) since these files hold auth tokens.
|
|
18
|
+
export function writeJsonFileAtomic(filePath, data) {
|
|
19
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
20
|
+
writeFileAtomic.sync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
21
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { EnvironmentConfig, LoopressConfig, ProjectConfig } from '
|
|
1
|
+
import { EnvironmentConfig, LoopressConfig, ProjectConfig } from '../types/config.js';
|
|
2
2
|
export declare class ProjectConfigManager {
|
|
3
3
|
private readonly homeDir;
|
|
4
|
-
private static instance;
|
|
5
4
|
constructor(homeDir?: string);
|
|
6
|
-
static getInstance(): ProjectConfigManager;
|
|
7
5
|
createProjectId(): string;
|
|
8
6
|
ensureConfigDir(): void;
|
|
9
7
|
getConfigFilePath(): string;
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { readJsonFile, writeJsonFileAtomic } from './json-file.js';
|
|
5
6
|
export class ProjectConfigManager {
|
|
6
7
|
homeDir;
|
|
7
|
-
static instance;
|
|
8
8
|
constructor(homeDir = homedir()) {
|
|
9
9
|
this.homeDir = homeDir;
|
|
10
10
|
}
|
|
11
|
-
static getInstance() {
|
|
12
|
-
if (!ProjectConfigManager.instance) {
|
|
13
|
-
ProjectConfigManager.instance = new ProjectConfigManager();
|
|
14
|
-
}
|
|
15
|
-
return ProjectConfigManager.instance;
|
|
16
|
-
}
|
|
17
11
|
createProjectId() {
|
|
18
12
|
return randomUUID();
|
|
19
13
|
}
|
|
@@ -73,17 +67,11 @@ export class ProjectConfigManager {
|
|
|
73
67
|
}));
|
|
74
68
|
}
|
|
75
69
|
readConfig() {
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
return { currentProject: null, projects: {} };
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
82
|
-
return this.sanitizeConfig(parsed);
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
70
|
+
const parsed = readJsonFile(this.getConfigFilePath());
|
|
71
|
+
if (parsed === null) {
|
|
85
72
|
return { currentProject: null, projects: {} };
|
|
86
73
|
}
|
|
74
|
+
return this.sanitizeConfig(parsed);
|
|
87
75
|
}
|
|
88
76
|
removeEnvironment(projectId, envName) {
|
|
89
77
|
const config = this.readConfig();
|
|
@@ -136,11 +124,7 @@ export class ProjectConfigManager {
|
|
|
136
124
|
this.writeConfig(config);
|
|
137
125
|
}
|
|
138
126
|
writeConfig(config) {
|
|
139
|
-
this.
|
|
140
|
-
const filePath = this.getConfigFilePath();
|
|
141
|
-
const tmpPath = `${filePath}.tmp`;
|
|
142
|
-
writeFileSync(tmpPath, JSON.stringify(config, null, 2));
|
|
143
|
-
renameSync(tmpPath, filePath);
|
|
127
|
+
writeJsonFileAtomic(this.getConfigFilePath(), config);
|
|
144
128
|
}
|
|
145
129
|
isProjectConfig(value) {
|
|
146
130
|
if (typeof value !== 'object' || value === null)
|
|
@@ -174,4 +158,4 @@ export class ProjectConfigManager {
|
|
|
174
158
|
return projects;
|
|
175
159
|
}
|
|
176
160
|
}
|
|
177
|
-
export const configManager = ProjectConfigManager
|
|
161
|
+
export const configManager = new ProjectConfigManager();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/node';
|
|
2
|
+
import { isTelemetryDisabled, runtimeContext } from '../lib/sentry.js';
|
|
3
|
+
// oclif has no `command_error` hook (checked @oclif/core@4.11.11's hooks.d.ts). `finally`
|
|
4
|
+
// is the closest equivalent: it always runs at the end of the CLI lifecycle and carries
|
|
5
|
+
// the error, if any, so it's where we report crashes before the process exits.
|
|
6
|
+
const hook = async function (options) {
|
|
7
|
+
if (!options.error || isTelemetryDisabled())
|
|
8
|
+
return;
|
|
9
|
+
try {
|
|
10
|
+
Sentry.captureException(options.error, {
|
|
11
|
+
contexts: { runtime: runtimeContext() },
|
|
12
|
+
extra: { argv: options.argv },
|
|
13
|
+
tags: { command: options.id },
|
|
14
|
+
});
|
|
15
|
+
await Sentry.flush(2000);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
this.debug('Failed to report error to Sentry: %O', error);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
export default hook;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/node';
|
|
2
|
+
import { consumeErrorReportingFlag, isTelemetryDisabled, resolveEnvironment, SENTRY_DSN } from '../lib/sentry.js';
|
|
3
|
+
const hook = async function (options) {
|
|
4
|
+
consumeErrorReportingFlag(options.argv);
|
|
5
|
+
if (isTelemetryDisabled())
|
|
6
|
+
return;
|
|
7
|
+
try {
|
|
8
|
+
Sentry.init({
|
|
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
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export default hook;
|
package/dist/lib/base.d.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { EnvironmentConfig } from '../config
|
|
2
|
+
import { EnvironmentConfig } from '../types/config.js';
|
|
3
|
+
import { LoopressLocalConfig } from '../utils/loopress-config.js';
|
|
4
|
+
import { WpClient } from './wp-client.js';
|
|
3
5
|
export declare abstract class LoopressCommand extends Command {
|
|
4
|
-
static
|
|
5
|
-
|
|
6
|
-
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
static dryRunFlag: {
|
|
7
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
8
|
};
|
|
9
|
+
protected dryRun: boolean;
|
|
10
|
+
protected localConfig: LoopressLocalConfig;
|
|
9
11
|
protected siteConfig: EnvironmentConfig;
|
|
10
|
-
|
|
12
|
+
private wpClient?;
|
|
13
|
+
protected get rootDir(): string;
|
|
14
|
+
protected get wp(): WpClient;
|
|
11
15
|
init(): Promise<void>;
|
|
12
|
-
protected resolveSnippetPlugin(flag?: string):
|
|
13
|
-
protected resolveSnippetsPath(override?: string):
|
|
16
|
+
protected resolveSnippetPlugin(flag?: string): 'code-snippets' | 'wpcode';
|
|
17
|
+
protected resolveSnippetsPath(override?: string): string;
|
|
18
|
+
private resolveEnvironment;
|
|
14
19
|
}
|
package/dist/lib/base.js
CHANGED
|
@@ -2,70 +2,72 @@ import { Command, Flags } from '@oclif/core';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { configManager } from '../config/project-config.manager.js';
|
|
4
4
|
import { readLocalConfig } from '../utils/loopress-config.js';
|
|
5
|
+
import { WpClient } from './wp-client.js';
|
|
5
6
|
export class LoopressCommand extends Command {
|
|
6
|
-
static
|
|
7
|
-
|
|
8
|
-
description: 'WordPress application password (fallback; prefer `lps project config`)',
|
|
9
|
-
helpGroup: 'GLOBAL',
|
|
10
|
-
}),
|
|
11
|
-
url: Flags.string({
|
|
12
|
-
description: 'WordPress URL (fallback; prefer `lps project config`)',
|
|
13
|
-
helpGroup: 'GLOBAL',
|
|
14
|
-
}),
|
|
15
|
-
user: Flags.string({
|
|
16
|
-
description: 'WordPress username (fallback; prefer `lps project config`)',
|
|
17
|
-
helpGroup: 'GLOBAL',
|
|
18
|
-
}),
|
|
7
|
+
static dryRunFlag = {
|
|
8
|
+
'dry-run': Flags.boolean({ char: 'd', description: 'Show what would change without making changes' }),
|
|
19
9
|
};
|
|
10
|
+
dryRun = false;
|
|
11
|
+
localConfig = {};
|
|
20
12
|
siteConfig;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
wpClient;
|
|
14
|
+
get rootDir() {
|
|
15
|
+
return this.localConfig.rootDir ?? '.';
|
|
16
|
+
}
|
|
17
|
+
get wp() {
|
|
18
|
+
if (!this.wpClient) {
|
|
19
|
+
const { token, url } = this.siteConfig;
|
|
20
|
+
if (!token) {
|
|
21
|
+
this.error(`No credentials configured for ${url}. Run \`lps project config\` to add them.`);
|
|
22
|
+
}
|
|
23
|
+
this.wpClient = new WpClient(url, token);
|
|
25
24
|
}
|
|
26
|
-
this.
|
|
25
|
+
return this.wpClient;
|
|
27
26
|
}
|
|
28
27
|
async init() {
|
|
29
28
|
await super.init();
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
const { flags } = (await this.parse({
|
|
30
|
+
args: this.ctor.args,
|
|
31
|
+
flags: this.ctor.flags,
|
|
32
|
+
strict: this.ctor.strict,
|
|
33
|
+
}));
|
|
34
|
+
this.dryRun = Boolean(flags['dry-run']);
|
|
35
|
+
this.localConfig = await readLocalConfig();
|
|
36
|
+
this.siteConfig = this.resolveEnvironment();
|
|
37
|
+
}
|
|
38
|
+
resolveSnippetPlugin(flag) {
|
|
39
|
+
if (flag)
|
|
40
|
+
return flag;
|
|
41
|
+
return this.localConfig.snippetPlugin ?? 'wpcode';
|
|
42
|
+
}
|
|
43
|
+
resolveSnippetsPath(override) {
|
|
44
|
+
if (override)
|
|
45
|
+
return override;
|
|
46
|
+
return join(this.rootDir, this.localConfig.snippetsDir ?? 'snippets');
|
|
47
|
+
}
|
|
48
|
+
resolveEnvironment() {
|
|
49
|
+
if (this.localConfig.projectId) {
|
|
50
|
+
const project = configManager.getProject(this.localConfig.projectId);
|
|
33
51
|
if (!project) {
|
|
34
|
-
this.error(`Project "${localConfig.projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
|
|
52
|
+
this.error(`Project "${this.localConfig.projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
|
|
35
53
|
}
|
|
36
54
|
const envNames = Object.keys(project.environments);
|
|
37
55
|
if (envNames.length === 0) {
|
|
38
56
|
this.error(`Project "${project.name}" has no environments configured. Run \`lps project config\` to add one.`);
|
|
39
57
|
}
|
|
40
58
|
if (envNames.length === 1) {
|
|
41
|
-
|
|
42
|
-
return;
|
|
59
|
+
return project.environments[envNames[0]];
|
|
43
60
|
}
|
|
44
61
|
const current = configManager.getCurrentProject();
|
|
45
|
-
const currentEnv = current?.id === localConfig.projectId ? configManager.getCurrentEnv() : null;
|
|
62
|
+
const currentEnv = current?.id === this.localConfig.projectId ? configManager.getCurrentEnv() : null;
|
|
46
63
|
if (!currentEnv) {
|
|
47
64
|
this.error(`Project "${project.name}" has multiple environments. Run \`lps project switch\` to pick one.`);
|
|
48
65
|
}
|
|
49
|
-
|
|
50
|
-
return;
|
|
66
|
+
return currentEnv;
|
|
51
67
|
}
|
|
52
68
|
const env = configManager.getCurrentEnv();
|
|
53
|
-
if (env)
|
|
54
|
-
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
69
|
+
if (env)
|
|
70
|
+
return env;
|
|
57
71
|
this.error('No environment configured. Run `lps project config` first.');
|
|
58
72
|
}
|
|
59
|
-
async resolveSnippetPlugin(flag) {
|
|
60
|
-
if (flag)
|
|
61
|
-
return flag;
|
|
62
|
-
const config = await readLocalConfig();
|
|
63
|
-
return config.snippetPlugin ?? 'wpcode';
|
|
64
|
-
}
|
|
65
|
-
async resolveSnippetsPath(override) {
|
|
66
|
-
if (override)
|
|
67
|
-
return override;
|
|
68
|
-
const config = await readLocalConfig();
|
|
69
|
-
return join(config.rootDir ?? '.', config.snippetsDir ?? 'snippets');
|
|
70
|
-
}
|
|
71
73
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { LoopressCommand } from './base.js';
|
|
2
2
|
export declare abstract class PushCommand extends LoopressCommand {
|
|
3
|
-
protected dryRun: boolean;
|
|
4
3
|
catch(err: Error): Promise<void>;
|
|
5
4
|
protected recordDeployment(status: 'failure' | 'success'): Promise<void>;
|
|
6
5
|
protected recordSuccess(): Promise<void>;
|
package/dist/lib/push-command.js
CHANGED
|
@@ -3,7 +3,6 @@ import { authManager } from '../config/auth.manager.js';
|
|
|
3
3
|
import { LoopressCommand } from './base.js';
|
|
4
4
|
const API_URL = process.env.LPS_API_URL ?? 'https://api.loopress.dev';
|
|
5
5
|
export class PushCommand extends LoopressCommand {
|
|
6
|
-
dryRun = false;
|
|
7
6
|
async catch(err) {
|
|
8
7
|
if (!this.dryRun && this.siteConfig) {
|
|
9
8
|
await this.recordDeployment('failure');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const SENTRY_DSN = "https://a08dd56bfffc2a45d5b8f665e4cb8b7d@o4511586904309760.ingest.de.sentry.io/4511673275973712";
|
|
2
|
+
export declare function consumeErrorReportingFlag(argv: string[]): void;
|
|
3
|
+
export declare function isTelemetryDisabled(): boolean;
|
|
4
|
+
export declare function resolveEnvironment(): string;
|
|
5
|
+
export declare function runtimeContext(): {
|
|
6
|
+
node: string;
|
|
7
|
+
os: string;
|
|
8
|
+
};
|