@loopress/cli 0.6.0 → 0.8.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 +98 -117
- package/dist/commands/composer/pull.js +5 -15
- package/dist/commands/composer/push.js +8 -17
- package/dist/commands/init.js +15 -6
- package/dist/commands/login.js +2 -2
- package/dist/commands/logout.js +2 -2
- package/dist/commands/plugin/add.d.ts +0 -2
- package/dist/commands/plugin/add.js +11 -32
- package/dist/commands/plugin/pull.js +8 -19
- package/dist/commands/plugin/push.d.ts +1 -0
- package/dist/commands/plugin/push.js +23 -48
- package/dist/commands/project/config.d.ts +1 -0
- package/dist/commands/project/config.js +36 -17
- package/dist/commands/project/list.js +4 -5
- package/dist/commands/project/remove.js +33 -14
- package/dist/commands/project/switch.d.ts +2 -0
- package/dist/commands/project/switch.js +28 -9
- package/dist/commands/snippet/list.d.ts +1 -1
- package/dist/commands/snippet/list.js +26 -41
- package/dist/commands/snippet/pull.d.ts +1 -1
- package/dist/commands/snippet/pull.js +45 -53
- package/dist/commands/snippet/push.d.ts +2 -2
- package/dist/commands/snippet/push.js +112 -68
- package/dist/commands/{project/remove-env.d.ts → status.d.ts} +3 -1
- package/dist/commands/status.js +66 -0
- 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 +18 -13
- package/dist/config/project-config.manager.js +90 -59
- package/dist/lib/base.d.ts +14 -4
- package/dist/lib/base.js +69 -29
- package/dist/lib/push-command.d.ts +0 -1
- package/dist/lib/push-command.js +0 -1
- package/dist/lib/wp-client.d.ts +15 -0
- package/dist/lib/wp-client.js +53 -0
- package/dist/{config/types.d.ts → types/config.d.ts} +5 -2
- package/dist/types/snippet.d.ts +8 -0
- 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 +24 -2
- package/dist/utils/snippet-plugin.js +170 -14
- package/oclif.manifest.json +122 -108
- package/package.json +21 -4
- package/dist/commands/project/remove-env.js +0 -33
- package/dist/commands/project/switch-env.d.ts +0 -6
- package/dist/commands/project/switch-env.js +0 -33
- 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,7 +1,9 @@
|
|
|
1
|
-
import { Args
|
|
2
|
-
import
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
import slugify from 'slugify';
|
|
4
5
|
import { LoopressCommand } from '../../lib/base.js';
|
|
6
|
+
import { snippetPluginFlag } from '../../utils/snippet-plugin-flag.js';
|
|
5
7
|
import { getSnippetPlugin } from '../../utils/snippet-plugin.js';
|
|
6
8
|
const EXTENSIONS = {
|
|
7
9
|
css: 'css',
|
|
@@ -22,11 +24,18 @@ export function buildMetaFile(snippet) {
|
|
|
22
24
|
name: snippet.name,
|
|
23
25
|
type: snippet.type,
|
|
24
26
|
active: snippet.active,
|
|
27
|
+
location: snippet.location,
|
|
25
28
|
};
|
|
26
29
|
if (snippet.description)
|
|
27
30
|
meta.description = snippet.description;
|
|
28
31
|
if (snippet.tags.length > 0)
|
|
29
32
|
meta.tags = snippet.tags;
|
|
33
|
+
if (snippet.insertMethod === 'shortcode')
|
|
34
|
+
meta.insertMethod = snippet.insertMethod;
|
|
35
|
+
if (snippet.priority !== 10)
|
|
36
|
+
meta.priority = snippet.priority;
|
|
37
|
+
if (snippet.shortcodeAttributes.length > 0)
|
|
38
|
+
meta.shortcodeAttributes = snippet.shortcodeAttributes;
|
|
30
39
|
return JSON.stringify(meta, null, 2) + '\n';
|
|
31
40
|
}
|
|
32
41
|
export default class Pull extends LoopressCommand {
|
|
@@ -35,66 +44,49 @@ export default class Pull extends LoopressCommand {
|
|
|
35
44
|
};
|
|
36
45
|
static description = 'Pull snippets from WordPress';
|
|
37
46
|
static examples = [
|
|
38
|
-
'$ lps
|
|
39
|
-
'$ lps
|
|
40
|
-
'$ lps
|
|
41
|
-
'$ lps
|
|
47
|
+
'$ lps snippet pull',
|
|
48
|
+
'$ lps snippet pull --url http://example.com',
|
|
49
|
+
'$ lps snippet pull --path ./snippets',
|
|
50
|
+
'$ lps snippet pull --plugin wpcode',
|
|
42
51
|
];
|
|
43
52
|
static flags = {
|
|
44
53
|
...LoopressCommand.baseFlags,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
char: 'p',
|
|
48
|
-
description: 'WordPress snippet plugin to target (overrides loopress.json)',
|
|
49
|
-
options: ['code-snippets', 'wpcode'],
|
|
50
|
-
}),
|
|
54
|
+
...LoopressCommand.dryRunFlag,
|
|
55
|
+
...snippetPluginFlag,
|
|
51
56
|
};
|
|
52
57
|
async run() {
|
|
53
58
|
const { args, flags } = await this.parse(Pull);
|
|
54
|
-
const { dryRun, plugin } = flags;
|
|
55
59
|
const { url } = this.siteConfig;
|
|
56
|
-
const path =
|
|
57
|
-
const resolvedPlugin =
|
|
58
|
-
this.log(
|
|
59
|
-
this.log(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let count = 0;
|
|
75
|
-
let skipped = 0;
|
|
76
|
-
for (const snippet of snippets) {
|
|
77
|
-
if (!snippet.name.trim()) {
|
|
78
|
-
skipped++;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
const ext = EXTENSIONS[snippet.type];
|
|
82
|
-
const slug = slugify(snippet.name, { lower: true, strict: true });
|
|
83
|
-
const base = `${snippet.id}-${slug}`;
|
|
84
|
-
const filePath = `${path}/${base}.${ext}`;
|
|
85
|
-
const metaPath = `${path}/${base}.json`;
|
|
86
|
-
await fs.writeFile(filePath, buildSnippetFile(snippet));
|
|
87
|
-
await fs.writeFile(metaPath, buildMetaFile(snippet));
|
|
88
|
-
count++;
|
|
89
|
-
this.log(`✅ Pulled: ${snippet.name}`);
|
|
90
|
-
}
|
|
91
|
-
this.log(`🎉 Successfully pulled ${count} snippet${count === 1 ? '' : 's'} to ${path}`);
|
|
92
|
-
if (skipped > 0) {
|
|
93
|
-
this.warn(`${skipped} snippet${skipped === 1 ? '' : 's'} skipped because they have no name`);
|
|
60
|
+
const path = this.resolveSnippetsPath(args.path);
|
|
61
|
+
const resolvedPlugin = this.resolveSnippetPlugin(flags.plugin);
|
|
62
|
+
this.log(`Pulling snippets from ${url} via ${resolvedPlugin}`);
|
|
63
|
+
this.log(`Snippets path: ${path}`);
|
|
64
|
+
const adapter = getSnippetPlugin(resolvedPlugin);
|
|
65
|
+
const remoteList = await this.wp.get(adapter.endpointPath());
|
|
66
|
+
const snippets = remoteList.map((r) => adapter.fromRemote(r));
|
|
67
|
+
if (this.dryRun) {
|
|
68
|
+
this.log(`[dry-run] Would pull ${snippets.length} snippet${snippets.length === 1 ? '' : 's'} to ${path}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await mkdir(path, { recursive: true });
|
|
72
|
+
let count = 0;
|
|
73
|
+
let skipped = 0;
|
|
74
|
+
for (const snippet of snippets) {
|
|
75
|
+
if (!snippet.name.trim()) {
|
|
76
|
+
skipped++;
|
|
77
|
+
continue;
|
|
94
78
|
}
|
|
79
|
+
const ext = EXTENSIONS[snippet.type];
|
|
80
|
+
const slug = slugify(snippet.name, { lower: true, strict: true });
|
|
81
|
+
const base = `${snippet.id}-${slug}`;
|
|
82
|
+
await writeFile(join(path, `${base}.${ext}`), buildSnippetFile(snippet));
|
|
83
|
+
await writeFile(join(path, `${base}.json`), buildMetaFile(snippet));
|
|
84
|
+
count++;
|
|
85
|
+
this.log(` Pulled: ${snippet.name}`);
|
|
95
86
|
}
|
|
96
|
-
|
|
97
|
-
|
|
87
|
+
this.log(`Pulled ${count} snippet${count === 1 ? '' : 's'} to ${path}`);
|
|
88
|
+
if (skipped > 0) {
|
|
89
|
+
this.warn(`${skipped} snippet${skipped === 1 ? '' : 's'} skipped because they have no name`);
|
|
98
90
|
}
|
|
99
91
|
}
|
|
100
92
|
}
|
|
@@ -6,14 +6,14 @@ export default class Push extends PushCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
-
dryRun: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
9
|
plugin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
password: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
14
|
};
|
|
15
15
|
run(): Promise<void>;
|
|
16
|
-
private
|
|
16
|
+
private ensureCanonicalFilename;
|
|
17
17
|
private loadSnippets;
|
|
18
18
|
private pushSnippet;
|
|
19
19
|
}
|
|
@@ -1,59 +1,69 @@
|
|
|
1
|
-
import { Args
|
|
2
|
-
import
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
4
|
+
import slugify from 'slugify';
|
|
3
5
|
import { PushCommand } from '../../lib/push-command.js';
|
|
4
|
-
import {
|
|
6
|
+
import { snippetPluginFlag } from '../../utils/snippet-plugin-flag.js';
|
|
7
|
+
import { defaultLocationForType, getSnippetPlugin, parseInsertMethod, parseLocation, parseType, } from '../../utils/snippet-plugin.js';
|
|
8
|
+
const TYPE_BY_EXTENSION = {
|
|
9
|
+
'.css': 'css',
|
|
10
|
+
'.html': 'html',
|
|
11
|
+
'.js': 'js',
|
|
12
|
+
'.php': 'php',
|
|
13
|
+
'.txt': 'text',
|
|
14
|
+
};
|
|
5
15
|
export default class Push extends PushCommand {
|
|
6
16
|
static args = {
|
|
7
17
|
path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
|
|
8
18
|
};
|
|
9
|
-
static description = 'Push snippets to WordPress';
|
|
19
|
+
static description = 'Push snippets to WordPress. Local snippet files created or updated remotely are renamed on disk to the `<id>-<slug>` convention.';
|
|
10
20
|
static examples = [
|
|
11
|
-
'$ lps
|
|
12
|
-
'$ lps
|
|
13
|
-
'$ lps
|
|
14
|
-
'$ lps
|
|
21
|
+
'$ lps snippet push',
|
|
22
|
+
'$ lps snippet push --url http://example.com',
|
|
23
|
+
'$ lps snippet push --path ./snippets',
|
|
24
|
+
'$ lps snippet push --plugin wpcode',
|
|
15
25
|
];
|
|
16
26
|
static flags = {
|
|
17
27
|
...PushCommand.baseFlags,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
char: 'p',
|
|
21
|
-
description: 'WordPress snippet plugin to target (overrides loopress.json)',
|
|
22
|
-
options: ['code-snippets', 'wpcode'],
|
|
23
|
-
}),
|
|
28
|
+
...PushCommand.dryRunFlag,
|
|
29
|
+
...snippetPluginFlag,
|
|
24
30
|
};
|
|
25
31
|
async run() {
|
|
26
32
|
const { args, flags } = await this.parse(Push);
|
|
27
|
-
const { dryRun, plugin } = flags;
|
|
28
|
-
this.dryRun = dryRun;
|
|
29
33
|
const { url } = this.siteConfig;
|
|
30
|
-
const path =
|
|
31
|
-
const resolvedPlugin =
|
|
32
|
-
this.log(
|
|
33
|
-
this.log(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
await this.pushSnippet(snippet, { adapter, dryRun, headers, url });
|
|
43
|
-
}
|
|
44
|
-
await this.recordSuccess();
|
|
45
|
-
this.log('🎉 All snippets pushed successfully!');
|
|
34
|
+
const path = this.resolveSnippetsPath(args.path);
|
|
35
|
+
const resolvedPlugin = this.resolveSnippetPlugin(flags.plugin);
|
|
36
|
+
this.log(`Pushing snippets to ${url} via ${resolvedPlugin}`);
|
|
37
|
+
this.log(`Snippets path: ${path}`);
|
|
38
|
+
const snippets = await this.loadSnippets(path);
|
|
39
|
+
this.log(`Found ${snippets.length} snippet${snippets.length === 1 ? '' : 's'} to push`);
|
|
40
|
+
const adapter = getSnippetPlugin(resolvedPlugin);
|
|
41
|
+
let failed = 0;
|
|
42
|
+
for (const snippet of snippets) {
|
|
43
|
+
const pushed = await this.pushSnippet(snippet, adapter);
|
|
44
|
+
if (!pushed)
|
|
45
|
+
failed++;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
this.error(
|
|
47
|
+
if (failed > 0) {
|
|
48
|
+
this.error(`${failed} snippet${failed === 1 ? '' : 's'} failed to push.`);
|
|
49
49
|
}
|
|
50
|
+
if (this.dryRun)
|
|
51
|
+
return;
|
|
52
|
+
await this.recordSuccess();
|
|
53
|
+
this.log('All snippets pushed.');
|
|
50
54
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
// Renames the local file pair to the `<id>-<slug>` convention used by `snippet pull` whenever
|
|
56
|
+
// it doesn't already match (e.g. a hand-created `demo.php` with no id, or a stale slug after a rename).
|
|
57
|
+
// This is a side effect of `push`: local files on disk are renamed, not just the remote snippet.
|
|
58
|
+
async ensureCanonicalFilename(snippet, id, name) {
|
|
59
|
+
const dir = dirname(snippet.path);
|
|
60
|
+
const ext = extname(snippet.path);
|
|
61
|
+
const currentBase = basename(snippet.path, ext);
|
|
62
|
+
const canonicalBase = `${id}-${slugify(name, { lower: true, strict: true })}`;
|
|
63
|
+
const oldMetaPath = join(dir, `${currentBase}.json`);
|
|
54
64
|
let meta = {};
|
|
55
65
|
try {
|
|
56
|
-
const existing = await
|
|
66
|
+
const existing = await readFile(oldMetaPath, 'utf8');
|
|
57
67
|
meta = JSON.parse(existing);
|
|
58
68
|
}
|
|
59
69
|
catch (error) {
|
|
@@ -61,70 +71,104 @@ export default class Push extends PushCommand {
|
|
|
61
71
|
throw error;
|
|
62
72
|
}
|
|
63
73
|
meta.id = id;
|
|
64
|
-
|
|
74
|
+
meta.name = name;
|
|
75
|
+
meta.type = snippet.type;
|
|
76
|
+
// Persist the id against the *current* file pairing before renaming anything, so a
|
|
77
|
+
// crash between the rename and the sidecar write still leaves a valid `<name>.<ext>` /
|
|
78
|
+
// `<name>.json` pair with the id on disk, and a retried `snippet push` won't re-create
|
|
79
|
+
// this snippet as a duplicate because it looks unlinked.
|
|
80
|
+
await writeFile(oldMetaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
81
|
+
if (currentBase === canonicalBase)
|
|
82
|
+
return;
|
|
83
|
+
const newPath = join(dir, `${canonicalBase}${ext}`);
|
|
84
|
+
const newMetaPath = join(dir, `${canonicalBase}.json`);
|
|
85
|
+
await rename(snippet.path, newPath);
|
|
86
|
+
await writeFile(newMetaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
87
|
+
await rm(oldMetaPath, { force: true });
|
|
88
|
+
this.log(` Renamed: ${snippet.path} → ${newPath}`);
|
|
65
89
|
}
|
|
66
90
|
async loadSnippets(path) {
|
|
67
|
-
const fs = await import('node:fs/promises');
|
|
68
91
|
const snippets = [];
|
|
69
|
-
const SNIPPET_EXTENSIONS = new Set(['.css', '.html', '.js', '.php', '.txt']);
|
|
70
92
|
try {
|
|
71
|
-
const files = await
|
|
93
|
+
const files = await readdir(path);
|
|
72
94
|
for (const file of files) {
|
|
73
|
-
const ext =
|
|
74
|
-
if (!
|
|
95
|
+
const ext = extname(file);
|
|
96
|
+
if (!(ext in TYPE_BY_EXTENSION))
|
|
75
97
|
continue;
|
|
76
|
-
const filePath =
|
|
77
|
-
const metaPath =
|
|
78
|
-
const content = await
|
|
98
|
+
const filePath = join(path, file);
|
|
99
|
+
const metaPath = join(path, `${basename(file, ext)}.json`);
|
|
100
|
+
const content = await readFile(filePath, 'utf8');
|
|
79
101
|
let id;
|
|
80
102
|
let name;
|
|
103
|
+
let type;
|
|
104
|
+
let active = false;
|
|
105
|
+
let tags = [];
|
|
106
|
+
let location = null;
|
|
107
|
+
let insertMethod = null;
|
|
108
|
+
let priority = 10;
|
|
109
|
+
let shortcodeAttributes = [];
|
|
81
110
|
try {
|
|
82
|
-
const metaContent = await
|
|
111
|
+
const metaContent = await readFile(metaPath, 'utf8');
|
|
83
112
|
const meta = JSON.parse(metaContent);
|
|
84
113
|
id = meta.id ? Number(meta.id) : undefined;
|
|
85
114
|
name = meta.name ? String(meta.name) : undefined;
|
|
115
|
+
type = parseType(meta.type) ?? undefined;
|
|
116
|
+
active = Boolean(meta.active);
|
|
117
|
+
tags = Array.isArray(meta.tags) ? meta.tags.map(String) : [];
|
|
118
|
+
location = parseLocation(meta.location);
|
|
119
|
+
insertMethod = parseInsertMethod(meta.insertMethod);
|
|
120
|
+
priority = meta.priority === undefined ? 10 : Number(meta.priority);
|
|
121
|
+
shortcodeAttributes = Array.isArray(meta.shortcodeAttributes) ? meta.shortcodeAttributes.map(String) : [];
|
|
86
122
|
}
|
|
87
123
|
catch (error) {
|
|
88
124
|
if (error.code !== 'ENOENT')
|
|
89
125
|
throw error;
|
|
90
126
|
}
|
|
127
|
+
const resolvedType = type ?? (ext in TYPE_BY_EXTENSION ? TYPE_BY_EXTENSION[ext] : 'php');
|
|
91
128
|
snippets.push({
|
|
129
|
+
active,
|
|
92
130
|
code: content,
|
|
93
131
|
id,
|
|
94
|
-
|
|
132
|
+
insertMethod: insertMethod ?? 'auto',
|
|
133
|
+
location: location ?? defaultLocationForType(resolvedType),
|
|
134
|
+
name: name ?? basename(file, ext),
|
|
95
135
|
path: filePath,
|
|
136
|
+
priority,
|
|
137
|
+
shortcodeAttributes,
|
|
138
|
+
tags,
|
|
139
|
+
type: resolvedType,
|
|
96
140
|
});
|
|
97
141
|
}
|
|
98
142
|
}
|
|
99
143
|
catch (error) {
|
|
100
|
-
this.error(
|
|
144
|
+
this.error(`Error loading snippets: ${error.message}`);
|
|
101
145
|
}
|
|
102
146
|
return snippets;
|
|
103
147
|
}
|
|
104
|
-
async pushSnippet(snippet,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this.log(`📄 Code preview: ${snippet.code.slice(0, 100)}...`);
|
|
109
|
-
return;
|
|
148
|
+
async pushSnippet(snippet, adapter) {
|
|
149
|
+
if (this.dryRun) {
|
|
150
|
+
this.log(`[dry-run] Would push: ${snippet.name}`);
|
|
151
|
+
return true;
|
|
110
152
|
}
|
|
153
|
+
const endpointPath = adapter.endpointPath();
|
|
111
154
|
try {
|
|
112
|
-
const
|
|
113
|
-
const payload = adapter.toPayload(snippet.name, snippet.code, snippet.path);
|
|
155
|
+
const payload = adapter.toPayload(snippet);
|
|
114
156
|
if (snippet.id) {
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
this.
|
|
118
|
-
|
|
157
|
+
await this.wp.put(`${endpointPath}/${snippet.id}`, payload);
|
|
158
|
+
this.log(` Updated: ${snippet.name} (id: ${snippet.id})`);
|
|
159
|
+
await this.ensureCanonicalFilename(snippet, snippet.id, snippet.name);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const response = await this.wp.post(endpointPath, payload);
|
|
163
|
+
const created = adapter.fromRemote(response);
|
|
164
|
+
this.log(` Created: ${snippet.name} (id: ${created.id})`);
|
|
165
|
+
await this.ensureCanonicalFilename(snippet, created.id, created.name);
|
|
119
166
|
}
|
|
120
|
-
|
|
121
|
-
const response = await got.post(endpoint, { headers, json: payload }).json();
|
|
122
|
-
const created = adapter.fromRemote(response);
|
|
123
|
-
await this.injectIdIntoMeta(snippet.path, created.id);
|
|
124
|
-
this.log(`✅ Created: ${snippet.name} (id: ${created.id})`);
|
|
167
|
+
return true;
|
|
125
168
|
}
|
|
126
169
|
catch (error) {
|
|
127
|
-
this.
|
|
170
|
+
this.warn(` Failed to push ${snippet.name}: ${error.message}`);
|
|
171
|
+
return false;
|
|
128
172
|
}
|
|
129
173
|
}
|
|
130
174
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
export default class
|
|
2
|
+
export default class Status extends Command {
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
run(): Promise<void>;
|
|
6
|
+
private reportActiveProject;
|
|
7
|
+
private reportPinnedProject;
|
|
6
8
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Command, ux } from '@oclif/core';
|
|
2
|
+
import { configManager } from '../config/project-config.manager.js';
|
|
3
|
+
import { readLocalConfig } from '../utils/loopress-config.js';
|
|
4
|
+
const c = ux.colorize;
|
|
5
|
+
export default class Status extends Command {
|
|
6
|
+
static description = 'Show which WordPress project and environment commands will target';
|
|
7
|
+
static examples = ['$ lps status'];
|
|
8
|
+
async run() {
|
|
9
|
+
await this.parse(Status);
|
|
10
|
+
const localConfig = await readLocalConfig();
|
|
11
|
+
if (localConfig.projectId) {
|
|
12
|
+
this.reportPinnedProject(localConfig.projectId);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.reportActiveProject();
|
|
16
|
+
}
|
|
17
|
+
reportActiveProject() {
|
|
18
|
+
const env = configManager.getCurrentEnv();
|
|
19
|
+
if (!env) {
|
|
20
|
+
this.log('No project configured. Run `lps project config` first.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const project = configManager.getCurrentProject();
|
|
24
|
+
if (!project) {
|
|
25
|
+
this.log('No project configured. Run `lps project config` first.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.log(`Project: ${project.name} (${env.name})`);
|
|
29
|
+
this.log(`URL: ${env.url}`);
|
|
30
|
+
}
|
|
31
|
+
reportPinnedProject(projectId) {
|
|
32
|
+
const project = configManager.getProject(projectId);
|
|
33
|
+
if (!project) {
|
|
34
|
+
this.log(`loopress.json pins project "${projectId}", but it no longer exists.`);
|
|
35
|
+
this.log('Run `lps project config` to configure it.');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const envNames = Object.keys(project.environments);
|
|
39
|
+
if (envNames.length === 0) {
|
|
40
|
+
this.log(`Project: ${project.name}`);
|
|
41
|
+
this.log('No environments configured for this project. Run `lps project config` to add one.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (envNames.length === 1) {
|
|
45
|
+
const env = project.environments[envNames[0]];
|
|
46
|
+
this.log(`Project: ${project.name} (${env.name})`);
|
|
47
|
+
this.log(`URL: ${env.url}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const current = configManager.getCurrentProject();
|
|
51
|
+
const currentEnv = current?.id === projectId ? configManager.getCurrentEnv() : null;
|
|
52
|
+
if (!currentEnv) {
|
|
53
|
+
this.log(`Project: ${project.name} ${c('yellow', '(ambiguous)')}`);
|
|
54
|
+
this.log(`Environments: ${envNames.join(', ')}`);
|
|
55
|
+
this.log('');
|
|
56
|
+
this.warn(`"${project.name}" has multiple environments and isn't the globally active project.`);
|
|
57
|
+
this.log('Run `lps project switch` to pick one before running commands here.');
|
|
58
|
+
if (current) {
|
|
59
|
+
this.log(`(Globally active project right now: "${current.name}")`);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.log(`Project: ${project.name} (${currentEnv.name})`);
|
|
64
|
+
this.log(`URL: ${currentEnv.url}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -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,28 +1,33 @@
|
|
|
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
|
-
|
|
5
|
+
createProjectId(): string;
|
|
7
6
|
ensureConfigDir(): void;
|
|
8
7
|
getConfigFilePath(): string;
|
|
9
8
|
getCurrentEnv(): EnvironmentConfig | null;
|
|
10
|
-
getCurrentProject(): null | ProjectConfig
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
getCurrentProject(): null | (ProjectConfig & {
|
|
10
|
+
id: string;
|
|
11
|
+
});
|
|
12
|
+
getEnvironment(projectId: string, envName: string): EnvironmentConfig | null;
|
|
13
|
+
getProject(id: string): null | ProjectConfig;
|
|
14
|
+
listEnvironments(projectId: string): Array<EnvironmentConfig & {
|
|
14
15
|
isCurrent: boolean;
|
|
15
16
|
}>;
|
|
16
17
|
listProjects(): Array<ProjectConfig & {
|
|
18
|
+
id: string;
|
|
17
19
|
isCurrent: boolean;
|
|
18
20
|
}>;
|
|
19
21
|
readConfig(): LoopressConfig;
|
|
20
|
-
removeEnvironment(
|
|
21
|
-
removeProject(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
setProject(name: string, project: ProjectConfig): void;
|
|
22
|
+
removeEnvironment(projectId: string, envName: string): void;
|
|
23
|
+
removeProject(id: string): void;
|
|
24
|
+
setCurrent(projectId: string, envName: string): void;
|
|
25
|
+
setEnvironment(projectId: string, envName: string, env: EnvironmentConfig): void;
|
|
26
|
+
setProject(id: string, project: ProjectConfig): void;
|
|
26
27
|
writeConfig(config: LoopressConfig): void;
|
|
28
|
+
private isProjectConfig;
|
|
29
|
+
private sanitizeConfig;
|
|
30
|
+
private sanitizeCurrentProject;
|
|
31
|
+
private sanitizeProjects;
|
|
27
32
|
}
|
|
28
33
|
export declare const configManager: ProjectConfigManager;
|