@loopress/cli 0.5.0 → 0.7.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 +140 -101
- package/dist/commands/composer/pull.d.ts +12 -0
- package/dist/commands/composer/pull.js +33 -0
- package/dist/commands/composer/push.d.ts +12 -0
- package/dist/commands/composer/push.js +49 -0
- package/dist/commands/init.js +15 -6
- package/dist/commands/login.js +1 -1
- package/dist/commands/logout.js +1 -1
- package/dist/commands/plugin/{require.d.ts → add.d.ts} +1 -1
- package/dist/commands/plugin/{require.js → add.js} +8 -8
- package/dist/commands/plugin/pull.js +11 -2
- package/dist/commands/plugin/push.js +13 -4
- 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.js +3 -3
- package/dist/commands/snippet/pull.d.ts +1 -1
- package/dist/commands/snippet/pull.js +7 -6
- package/dist/commands/snippet/push.d.ts +2 -2
- package/dist/commands/snippet/push.js +47 -15
- package/dist/commands/{project/switch-env.d.ts → status.d.ts} +3 -1
- package/dist/commands/status.js +66 -0
- package/dist/config/project-config.manager.d.ts +17 -10
- package/dist/config/project-config.manager.js +91 -44
- package/dist/config/types.d.ts +5 -2
- package/dist/lib/base.js +13 -3
- package/dist/types/snippet.d.ts +2 -0
- package/dist/utils/composer.d.ts +7 -0
- package/dist/utils/composer.js +33 -0
- package/dist/utils/snippet-plugin.d.ts +2 -1
- package/dist/utils/snippet-plugin.js +5 -4
- package/oclif.manifest.json +160 -72
- package/package.json +25 -6
- package/dist/commands/project/remove-env.d.ts +0 -6
- package/dist/commands/project/remove-env.js +0 -33
- package/dist/commands/project/switch-env.js +0 -33
|
@@ -24,23 +24,23 @@ export async function resolvePluginVersion(slug, version) {
|
|
|
24
24
|
throw new Error(`Plugin "${slug}" not found on WordPress.org.`);
|
|
25
25
|
return info.version;
|
|
26
26
|
}
|
|
27
|
-
export default class
|
|
27
|
+
export default class Add extends LoopressCommand {
|
|
28
28
|
static args = {
|
|
29
|
-
slug: Args.string({ description: 'Plugin slug
|
|
29
|
+
slug: Args.string({ description: 'Plugin slug on WordPress.org', required: true }),
|
|
30
30
|
version: Args.string({ description: 'Version to pin (default: latest)' }),
|
|
31
31
|
};
|
|
32
|
-
static description = 'Add a plugin to loopress.json
|
|
32
|
+
static description = 'Add a WordPress.org plugin to loopress.json';
|
|
33
33
|
static examples = [
|
|
34
|
-
'$ lps
|
|
35
|
-
'$ lps
|
|
36
|
-
'$ lps
|
|
34
|
+
'$ lps plugin add woocommerce',
|
|
35
|
+
'$ lps plugin add woocommerce 8.9.1',
|
|
36
|
+
'$ lps plugin add contact-form-7 --dry-run',
|
|
37
37
|
];
|
|
38
38
|
static flags = {
|
|
39
39
|
...LoopressCommand.baseFlags,
|
|
40
40
|
'dry-run': Flags.boolean({ char: 'd', description: 'Show what would be written without making changes' }),
|
|
41
41
|
};
|
|
42
42
|
async run() {
|
|
43
|
-
const { args, flags } = await this.parse(
|
|
43
|
+
const { args, flags } = await this.parse(Add);
|
|
44
44
|
const dryRun = flags['dry-run'];
|
|
45
45
|
const { slug } = args;
|
|
46
46
|
const requestedVersion = args.version ?? 'latest';
|
|
@@ -56,7 +56,7 @@ export default class Require extends LoopressCommand {
|
|
|
56
56
|
const localConfig = await readLocalConfig();
|
|
57
57
|
const existing = localConfig.plugins ?? {};
|
|
58
58
|
if (existing[slug] === resolvedVersion) {
|
|
59
|
-
this.log(`${slug}@${resolvedVersion} is already in loopress.json
|
|
59
|
+
this.log(`${slug}@${resolvedVersion} is already in loopress.json, nothing to do.`);
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
const updated = existing[slug] !== undefined;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import got from 'got';
|
|
3
3
|
import { LoopressCommand } from '../../lib/base.js';
|
|
4
|
+
import { getComposerManagedSlugs, readComposerJson } from '../../utils/composer.js';
|
|
4
5
|
import { readLocalConfig, writeLocalConfig } from '../../utils/loopress-config.js';
|
|
5
6
|
import { mergePluginManifest } from '../../utils/plugins.js';
|
|
6
7
|
export default class Pull extends LoopressCommand {
|
|
7
8
|
static description = 'Pull installed plugins from WordPress into loopress.json';
|
|
8
|
-
static examples = ['$ lps
|
|
9
|
+
static examples = ['$ lps plugin pull', '$ lps plugin pull --dry-run'];
|
|
9
10
|
static flags = {
|
|
10
11
|
...LoopressCommand.baseFlags,
|
|
11
12
|
'dry-run': Flags.boolean({ char: 'd', description: 'Show what would be written without making changes' }),
|
|
@@ -17,7 +18,15 @@ export default class Pull extends LoopressCommand {
|
|
|
17
18
|
this.log(`Pulling plugins from ${url}`);
|
|
18
19
|
const headers = await this.buildAuthHeaders();
|
|
19
20
|
const installed = await got.get(`${url}/wp-json/loopress/v1/plugins`, { headers }).json();
|
|
20
|
-
const
|
|
21
|
+
const composerJson = await readComposerJson();
|
|
22
|
+
const composerSlugs = composerJson ? getComposerManagedSlugs(composerJson) : [];
|
|
23
|
+
const incoming = Object.fromEntries(installed.filter((p) => !composerSlugs.includes(p.slug)).map((p) => [p.slug, p.version]));
|
|
24
|
+
if (composerSlugs.length > 0) {
|
|
25
|
+
const found = installed.filter((p) => composerSlugs.includes(p.slug)).map((p) => p.slug);
|
|
26
|
+
if (found.length > 0) {
|
|
27
|
+
this.log(`Skipping ${found.length} Composer-managed ${found.length === 1 ? 'plugin' : 'plugins'}: ${found.join(', ')}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
21
30
|
const localConfig = await readLocalConfig();
|
|
22
31
|
const { added, merged, updated } = mergePluginManifest(localConfig.plugins ?? {}, incoming);
|
|
23
32
|
if (dryRun) {
|
|
@@ -2,11 +2,12 @@ import { confirm } from '@inquirer/prompts';
|
|
|
2
2
|
import { Flags } from '@oclif/core';
|
|
3
3
|
import got from 'got';
|
|
4
4
|
import { PushCommand } from '../../lib/push-command.js';
|
|
5
|
+
import { getComposerManagedSlugs, readComposerJson } from '../../utils/composer.js';
|
|
5
6
|
import { readLocalConfig } from '../../utils/loopress-config.js';
|
|
6
7
|
import { diffPlugins } from '../../utils/plugins.js';
|
|
7
8
|
export default class Push extends PushCommand {
|
|
8
|
-
static description = '
|
|
9
|
-
static examples = ['$ lps
|
|
9
|
+
static description = 'Push plugins to WordPress to match loopress.json';
|
|
10
|
+
static examples = ['$ lps plugin push', '$ lps plugin push --dry-run'];
|
|
10
11
|
static flags = {
|
|
11
12
|
...PushCommand.baseFlags,
|
|
12
13
|
'dry-run': Flags.boolean({ char: 'd', description: 'Show what would change without making changes' }),
|
|
@@ -19,12 +20,20 @@ export default class Push extends PushCommand {
|
|
|
19
20
|
const localConfig = await readLocalConfig();
|
|
20
21
|
const manifest = localConfig.plugins;
|
|
21
22
|
if (!manifest || Object.keys(manifest).length === 0) {
|
|
22
|
-
this.error('No plugins found in loopress.json. Run `lps
|
|
23
|
+
this.error('No plugins found in loopress.json. Run `lps plugin pull` first.');
|
|
24
|
+
}
|
|
25
|
+
const composerJson = await readComposerJson();
|
|
26
|
+
const composerSlugs = composerJson ? getComposerManagedSlugs(composerJson) : [];
|
|
27
|
+
const filteredManifest = Object.fromEntries(Object.entries(manifest).filter(([slug]) => !composerSlugs.includes(slug)));
|
|
28
|
+
const skipped = composerSlugs.filter((slug) => slug in manifest);
|
|
29
|
+
if (skipped.length > 0) {
|
|
30
|
+
this.log(`Skipping ${skipped.length} Composer-managed ${skipped.length === 1 ? 'plugin' : 'plugins'}: ${skipped.join(', ')}`);
|
|
31
|
+
this.log('Run `lps composer push` to deploy them.');
|
|
23
32
|
}
|
|
24
33
|
this.log(`Pushing plugins to ${url}`);
|
|
25
34
|
const headers = await this.buildAuthHeaders();
|
|
26
35
|
const installed = await got.get(`${url}/wp-json/loopress/v1/plugins`, { headers }).json();
|
|
27
|
-
const { drifted, toActivate, toInstall } = diffPlugins(
|
|
36
|
+
const { drifted, toActivate, toInstall } = diffPlugins(filteredManifest, installed);
|
|
28
37
|
if (toInstall.length === 0 && toActivate.length === 0 && drifted.length === 0) {
|
|
29
38
|
this.log('Everything is already in sync.');
|
|
30
39
|
return;
|
|
@@ -1,20 +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
|
+
const NEW_PROJECT = '__new__';
|
|
4
5
|
export default class Config extends Command {
|
|
5
6
|
static description = 'Add or update a WordPress project environment';
|
|
6
7
|
static examples = ['$ lps project config'];
|
|
7
8
|
async run() {
|
|
8
9
|
await this.parse(Config);
|
|
9
|
-
const projectName = await
|
|
10
|
-
message: 'Project name (identifier, no spaces)',
|
|
11
|
-
validate(value) {
|
|
12
|
-
if (!/^[a-z0-9_-]+$/.test(value)) {
|
|
13
|
-
return 'Name must be lowercase with only letters, numbers, - and _';
|
|
14
|
-
}
|
|
15
|
-
return true;
|
|
16
|
-
},
|
|
17
|
-
});
|
|
10
|
+
const { projectId, projectName } = await this.resolveProject();
|
|
18
11
|
const envChoice = await select({
|
|
19
12
|
choices: [
|
|
20
13
|
{ name: 'local', value: 'local' },
|
|
@@ -30,8 +23,7 @@ export default class Config extends Command {
|
|
|
30
23
|
validate: (value) => (value.trim().length > 0 ? true : 'Name cannot be empty'),
|
|
31
24
|
})
|
|
32
25
|
: envChoice;
|
|
33
|
-
const
|
|
34
|
-
const existingEnv = existingProject?.environments[envName];
|
|
26
|
+
const existingEnv = configManager.getEnvironment(projectId, envName);
|
|
35
27
|
if (existingEnv) {
|
|
36
28
|
const overwrite = await confirm({
|
|
37
29
|
default: false,
|
|
@@ -73,20 +65,47 @@ export default class Config extends Command {
|
|
|
73
65
|
token,
|
|
74
66
|
url,
|
|
75
67
|
};
|
|
76
|
-
if (
|
|
77
|
-
configManager.setEnvironment(
|
|
68
|
+
if (configManager.getProject(projectId)) {
|
|
69
|
+
configManager.setEnvironment(projectId, envName, env);
|
|
78
70
|
}
|
|
79
71
|
else {
|
|
80
72
|
const project = {
|
|
81
73
|
addedAt: new Date().toISOString(),
|
|
82
|
-
currentEnv: envName,
|
|
83
74
|
environments: { [envName]: env },
|
|
84
75
|
name: projectName,
|
|
85
76
|
};
|
|
86
|
-
configManager.setProject(
|
|
77
|
+
configManager.setProject(projectId, project);
|
|
87
78
|
}
|
|
88
79
|
this.log(`✓ "${projectName}/${envName}" configured`);
|
|
89
|
-
this.log('→ Run `lps project switch` to change active project');
|
|
90
|
-
|
|
80
|
+
this.log('→ Run `lps project switch` to change the active project or environment');
|
|
81
|
+
}
|
|
82
|
+
async resolveProject() {
|
|
83
|
+
const projects = configManager.listProjects();
|
|
84
|
+
if (projects.length > 0) {
|
|
85
|
+
const choice = await select({
|
|
86
|
+
choices: [
|
|
87
|
+
...projects.map((project) => ({ name: project.name, value: project.id })),
|
|
88
|
+
{ name: 'Add a new project…', value: NEW_PROJECT },
|
|
89
|
+
],
|
|
90
|
+
message: 'Project',
|
|
91
|
+
});
|
|
92
|
+
if (choice !== NEW_PROJECT) {
|
|
93
|
+
const project = projects.find((p) => p.id === choice);
|
|
94
|
+
return { projectId: project.id, projectName: project.name };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const existingNames = new Set(projects.map((project) => project.name.trim().toLowerCase()));
|
|
98
|
+
const projectName = await input({
|
|
99
|
+
message: 'Project name',
|
|
100
|
+
validate(value) {
|
|
101
|
+
const trimmed = value.trim();
|
|
102
|
+
if (trimmed.length === 0)
|
|
103
|
+
return 'Name cannot be empty';
|
|
104
|
+
if (existingNames.has(trimmed.toLowerCase()))
|
|
105
|
+
return `A project named "${trimmed}" already exists`;
|
|
106
|
+
return true;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
return { projectId: configManager.createProjectId(), projectName: projectName.trim() };
|
|
91
110
|
}
|
|
92
111
|
}
|
|
@@ -12,17 +12,16 @@ export default class List extends Command {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
for (const project of projects) {
|
|
15
|
-
const envs =
|
|
15
|
+
const envs = configManager.listEnvironments(project.id);
|
|
16
16
|
const marker = project.isCurrent ? c('green', '●') : c('dim', '○');
|
|
17
17
|
const name = project.isCurrent ? c('green', project.name) : project.name;
|
|
18
18
|
const currentTag = project.isCurrent ? ` ${c('green', '[current]')}` : '';
|
|
19
19
|
this.log(`${marker} ${name}${currentTag}`);
|
|
20
20
|
for (const env of envs) {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const envName = isActiveEnv ? c('cyan', env.name.padEnd(15)) : c('dim', env.name.padEnd(15));
|
|
21
|
+
const envMarker = env.isCurrent ? c('cyan', '·') : c('dim', '·');
|
|
22
|
+
const envName = env.isCurrent ? c('cyan', env.name.padEnd(15)) : c('dim', env.name.padEnd(15));
|
|
24
23
|
const envUrl = c('dim', env.url);
|
|
25
|
-
const activeTag =
|
|
24
|
+
const activeTag = env.isCurrent ? ` ${c('cyan', '←')}` : '';
|
|
26
25
|
this.log(` ${envMarker} ${envName} ${envUrl}${activeTag}`);
|
|
27
26
|
}
|
|
28
27
|
this.log('');
|
|
@@ -2,7 +2,7 @@ import { checkbox } from '@inquirer/prompts';
|
|
|
2
2
|
import { Command } from '@oclif/core';
|
|
3
3
|
import { configManager } from '../../config/project-config.manager.js';
|
|
4
4
|
export default class Remove extends Command {
|
|
5
|
-
static description = 'Remove one or more WordPress
|
|
5
|
+
static description = 'Remove one or more WordPress projects or environments';
|
|
6
6
|
static examples = ['$ lps project remove'];
|
|
7
7
|
async run() {
|
|
8
8
|
await this.parse(Remove);
|
|
@@ -10,25 +10,44 @@ export default class Remove extends Command {
|
|
|
10
10
|
if (projects.length === 0) {
|
|
11
11
|
this.error('No projects configured.');
|
|
12
12
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const targets = [];
|
|
14
|
+
const choices = projects.flatMap((project) => {
|
|
15
|
+
const envCount = Object.keys(project.environments).length;
|
|
16
|
+
const envLabel = `${envCount} env${envCount > 1 ? 's' : ''}`;
|
|
17
|
+
const currentMarker = project.isCurrent ? ' [current]' : '';
|
|
18
|
+
targets.push({ kind: 'project', projectId: project.id, projectName: project.name });
|
|
19
|
+
const projectChoice = {
|
|
20
|
+
name: `${project.isCurrent ? '●' : '○'} ${project.name.padEnd(20)} (${envLabel})${currentMarker}`,
|
|
21
|
+
value: String(targets.length - 1),
|
|
22
|
+
};
|
|
23
|
+
const envChoices = configManager.listEnvironments(project.id).map((env) => {
|
|
24
|
+
targets.push({ env: env.name, kind: 'env', projectId: project.id, projectName: project.name });
|
|
18
25
|
return {
|
|
19
|
-
name:
|
|
20
|
-
value:
|
|
26
|
+
name: ` ${env.isCurrent ? '●' : '○'} ${env.name.padEnd(20)} ${env.url}${env.isCurrent ? ' [current]' : ''}`,
|
|
27
|
+
value: String(targets.length - 1),
|
|
21
28
|
};
|
|
22
|
-
})
|
|
23
|
-
|
|
29
|
+
});
|
|
30
|
+
return [projectChoice, ...envChoices];
|
|
31
|
+
});
|
|
32
|
+
const chosen = await checkbox({
|
|
33
|
+
choices,
|
|
34
|
+
message: 'Select projects or environments to remove',
|
|
24
35
|
});
|
|
25
36
|
if (chosen.length === 0) {
|
|
26
37
|
this.log('Nothing removed.');
|
|
27
38
|
return;
|
|
28
39
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
const selected = chosen.map((index) => targets[Number(index)]);
|
|
41
|
+
const projectsToRemove = new Map(selected.filter((target) => target.kind === 'project').map((target) => [target.projectId, target.projectName]));
|
|
42
|
+
const envsToRemove = selected.filter((target) => target.kind === 'env' && !projectsToRemove.has(target.projectId));
|
|
43
|
+
for (const projectId of projectsToRemove.keys())
|
|
44
|
+
configManager.removeProject(projectId);
|
|
45
|
+
for (const { env, projectId } of envsToRemove)
|
|
46
|
+
configManager.removeEnvironment(projectId, env);
|
|
47
|
+
const removedLabels = [
|
|
48
|
+
...projectsToRemove.values(),
|
|
49
|
+
...envsToRemove.map(({ env, projectName }) => `${projectName}/${env}`),
|
|
50
|
+
];
|
|
51
|
+
this.log(`✓ Removed: ${removedLabels.join(', ')}`);
|
|
33
52
|
}
|
|
34
53
|
}
|
|
@@ -2,7 +2,7 @@ import { select } from '@inquirer/prompts';
|
|
|
2
2
|
import { Command } from '@oclif/core';
|
|
3
3
|
import { configManager } from '../../config/project-config.manager.js';
|
|
4
4
|
export default class Switch extends Command {
|
|
5
|
-
static description = 'Switch the active project';
|
|
5
|
+
static description = 'Switch the active project and environment';
|
|
6
6
|
static examples = ['$ lps project switch'];
|
|
7
7
|
async run() {
|
|
8
8
|
await this.parse(Switch);
|
|
@@ -10,11 +10,30 @@ export default class Switch extends Command {
|
|
|
10
10
|
if (projects.length === 0) {
|
|
11
11
|
this.error('No projects configured. Run `lps project config` first.');
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const { id: projectId, name: projectName } = await this.resolveProject(projects);
|
|
14
|
+
const envName = await this.resolveEnvironment(projectId, projectName);
|
|
15
|
+
configManager.setCurrent(projectId, envName);
|
|
16
|
+
this.log(`✓ Switched to "${projectName}/${envName}"`);
|
|
17
|
+
}
|
|
18
|
+
async resolveEnvironment(projectId, projectName) {
|
|
19
|
+
const envs = configManager.listEnvironments(projectId);
|
|
20
|
+
if (envs.length === 0) {
|
|
21
|
+
this.error(`No environments configured for "${projectName}". Run \`lps project config\` first.`);
|
|
17
22
|
}
|
|
23
|
+
if (envs.length === 1)
|
|
24
|
+
return envs[0].name;
|
|
25
|
+
return select({
|
|
26
|
+
choices: envs.map((env) => ({
|
|
27
|
+
name: `${env.isCurrent ? '●' : '○'} ${env.name.padEnd(20)} ${env.url}${env.isCurrent ? ' [current]' : ''}`,
|
|
28
|
+
value: env.name,
|
|
29
|
+
})),
|
|
30
|
+
default: envs.find((env) => env.isCurrent)?.name,
|
|
31
|
+
message: `Select environment for "${projectName}"`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async resolveProject(projects) {
|
|
35
|
+
if (projects.length === 1)
|
|
36
|
+
return { id: projects[0].id, name: projects[0].name };
|
|
18
37
|
const chosen = await select({
|
|
19
38
|
choices: projects.map((project) => {
|
|
20
39
|
const envCount = Object.keys(project.environments).length;
|
|
@@ -22,13 +41,13 @@ export default class Switch extends Command {
|
|
|
22
41
|
const currentMarker = project.isCurrent ? ' [current]' : '';
|
|
23
42
|
return {
|
|
24
43
|
name: `${project.isCurrent ? '●' : '○'} ${project.name.padEnd(20)} (${envLabel})${currentMarker}`,
|
|
25
|
-
value: project.
|
|
44
|
+
value: project.id,
|
|
26
45
|
};
|
|
27
46
|
}),
|
|
28
|
-
default: projects.find((
|
|
47
|
+
default: projects.find((project) => project.isCurrent)?.id,
|
|
29
48
|
message: 'Select active project',
|
|
30
49
|
});
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
const project = projects.find((p) => p.id === chosen);
|
|
51
|
+
return { id: project.id, name: project.name };
|
|
33
52
|
}
|
|
34
53
|
}
|
|
@@ -5,9 +5,9 @@ import { getSnippetPlugin } from '../../utils/snippet-plugin.js';
|
|
|
5
5
|
export default class List extends LoopressCommand {
|
|
6
6
|
static description = 'List snippets from WordPress';
|
|
7
7
|
static examples = [
|
|
8
|
-
'$ lps
|
|
9
|
-
'$ lps
|
|
10
|
-
'$ lps
|
|
8
|
+
'$ lps snippet list',
|
|
9
|
+
'$ lps snippet list --url http://example.com',
|
|
10
|
+
'$ lps snippet list --plugin wpcode',
|
|
11
11
|
];
|
|
12
12
|
static flags = {
|
|
13
13
|
...LoopressCommand.baseFlags,
|
|
@@ -9,7 +9,7 @@ export default class Pull extends LoopressCommand {
|
|
|
9
9
|
static description: string;
|
|
10
10
|
static examples: string[];
|
|
11
11
|
static flags: {
|
|
12
|
-
|
|
12
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
13
|
plugin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
14
|
password: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
15
|
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -35,14 +35,14 @@ export default class Pull extends LoopressCommand {
|
|
|
35
35
|
};
|
|
36
36
|
static description = 'Pull snippets from WordPress';
|
|
37
37
|
static examples = [
|
|
38
|
-
'$ lps
|
|
39
|
-
'$ lps
|
|
40
|
-
'$ lps
|
|
41
|
-
'$ lps
|
|
38
|
+
'$ lps snippet pull',
|
|
39
|
+
'$ lps snippet pull --url http://example.com',
|
|
40
|
+
'$ lps snippet pull --path ./snippets',
|
|
41
|
+
'$ lps snippet pull --plugin wpcode',
|
|
42
42
|
];
|
|
43
43
|
static flags = {
|
|
44
44
|
...LoopressCommand.baseFlags,
|
|
45
|
-
|
|
45
|
+
'dry-run': Flags.boolean({ char: 'd', description: 'Show what would be written without making changes' }),
|
|
46
46
|
plugin: Flags.string({
|
|
47
47
|
char: 'p',
|
|
48
48
|
description: 'WordPress snippet plugin to target (overrides loopress.json)',
|
|
@@ -51,7 +51,8 @@ export default class Pull extends LoopressCommand {
|
|
|
51
51
|
};
|
|
52
52
|
async run() {
|
|
53
53
|
const { args, flags } = await this.parse(Pull);
|
|
54
|
-
const
|
|
54
|
+
const dryRun = flags['dry-run'];
|
|
55
|
+
const { plugin } = flags;
|
|
55
56
|
const { url } = this.siteConfig;
|
|
56
57
|
const path = await this.resolveSnippetsPath(args.path);
|
|
57
58
|
const resolvedPlugin = await this.resolveSnippetPlugin(plugin);
|
|
@@ -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
|
-
|
|
9
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
plugin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
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,21 +1,30 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import got from 'got';
|
|
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 { getSnippetPlugin } from '../../utils/snippet-plugin.js';
|
|
6
|
+
import { getSnippetPlugin, parseType } from '../../utils/snippet-plugin.js';
|
|
7
|
+
const TYPE_BY_EXTENSION = {
|
|
8
|
+
'.css': 'css',
|
|
9
|
+
'.html': 'html',
|
|
10
|
+
'.js': 'js',
|
|
11
|
+
'.php': 'php',
|
|
12
|
+
'.txt': 'text',
|
|
13
|
+
};
|
|
5
14
|
export default class Push extends PushCommand {
|
|
6
15
|
static args = {
|
|
7
16
|
path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
|
|
8
17
|
};
|
|
9
|
-
static description = 'Push snippets to WordPress';
|
|
18
|
+
static description = 'Push snippets to WordPress. Local snippet files created or updated remotely are renamed on disk to the `<id>-<slug>` convention.';
|
|
10
19
|
static examples = [
|
|
11
|
-
'$ lps
|
|
12
|
-
'$ lps
|
|
13
|
-
'$ lps
|
|
14
|
-
'$ lps
|
|
20
|
+
'$ lps snippet push',
|
|
21
|
+
'$ lps snippet push --url http://example.com',
|
|
22
|
+
'$ lps snippet push --path ./snippets',
|
|
23
|
+
'$ lps snippet push --plugin wpcode',
|
|
15
24
|
];
|
|
16
25
|
static flags = {
|
|
17
26
|
...PushCommand.baseFlags,
|
|
18
|
-
|
|
27
|
+
'dry-run': Flags.boolean({ char: 'd', description: 'Show what would change without making changes' }),
|
|
19
28
|
plugin: Flags.string({
|
|
20
29
|
char: 'p',
|
|
21
30
|
description: 'WordPress snippet plugin to target (overrides loopress.json)',
|
|
@@ -24,7 +33,8 @@ export default class Push extends PushCommand {
|
|
|
24
33
|
};
|
|
25
34
|
async run() {
|
|
26
35
|
const { args, flags } = await this.parse(Push);
|
|
27
|
-
const
|
|
36
|
+
const dryRun = flags['dry-run'];
|
|
37
|
+
const { plugin } = flags;
|
|
28
38
|
this.dryRun = dryRun;
|
|
29
39
|
const { url } = this.siteConfig;
|
|
30
40
|
const path = await this.resolveSnippetsPath(args.path);
|
|
@@ -48,12 +58,19 @@ export default class Push extends PushCommand {
|
|
|
48
58
|
this.error(error.message);
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
|
-
|
|
61
|
+
// Renames the local file pair to the `<id>-<slug>` convention used by `snippet pull` whenever
|
|
62
|
+
// it doesn't already match (e.g. a hand-created `demo.php` with no id, or a stale slug after a rename).
|
|
63
|
+
// This is a side effect of `push`: local files on disk are renamed, not just the remote snippet.
|
|
64
|
+
async ensureCanonicalFilename(snippet, id, name) {
|
|
52
65
|
const fs = await import('node:fs/promises');
|
|
53
|
-
const
|
|
66
|
+
const dir = dirname(snippet.path);
|
|
67
|
+
const ext = extname(snippet.path);
|
|
68
|
+
const currentBase = basename(snippet.path, ext);
|
|
69
|
+
const canonicalBase = `${id}-${slugify(name, { lower: true, strict: true })}`;
|
|
70
|
+
const oldMetaPath = join(dir, `${currentBase}.json`);
|
|
54
71
|
let meta = {};
|
|
55
72
|
try {
|
|
56
|
-
const existing = await fs.readFile(
|
|
73
|
+
const existing = await fs.readFile(oldMetaPath, 'utf8');
|
|
57
74
|
meta = JSON.parse(existing);
|
|
58
75
|
}
|
|
59
76
|
catch (error) {
|
|
@@ -61,12 +78,23 @@ export default class Push extends PushCommand {
|
|
|
61
78
|
throw error;
|
|
62
79
|
}
|
|
63
80
|
meta.id = id;
|
|
64
|
-
|
|
81
|
+
meta.name = name;
|
|
82
|
+
if (currentBase === canonicalBase) {
|
|
83
|
+
await fs.writeFile(oldMetaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const newPath = join(dir, `${canonicalBase}${ext}`);
|
|
87
|
+
const newMetaPath = join(dir, `${canonicalBase}.json`);
|
|
88
|
+
await fs.rename(snippet.path, newPath);
|
|
89
|
+
await fs.writeFile(newMetaPath, JSON.stringify(meta, null, 2) + '\n');
|
|
90
|
+
if (oldMetaPath !== newMetaPath)
|
|
91
|
+
await fs.rm(oldMetaPath, { force: true });
|
|
92
|
+
this.log(`📁 Renamed: ${snippet.path} → ${newPath}`);
|
|
65
93
|
}
|
|
66
94
|
async loadSnippets(path) {
|
|
67
95
|
const fs = await import('node:fs/promises');
|
|
68
96
|
const snippets = [];
|
|
69
|
-
const SNIPPET_EXTENSIONS = new Set(
|
|
97
|
+
const SNIPPET_EXTENSIONS = new Set(Object.keys(TYPE_BY_EXTENSION));
|
|
70
98
|
try {
|
|
71
99
|
const files = await fs.readdir(path);
|
|
72
100
|
for (const file of files) {
|
|
@@ -78,11 +106,13 @@ export default class Push extends PushCommand {
|
|
|
78
106
|
const content = await fs.readFile(filePath, 'utf8');
|
|
79
107
|
let id;
|
|
80
108
|
let name;
|
|
109
|
+
let type;
|
|
81
110
|
try {
|
|
82
111
|
const metaContent = await fs.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;
|
|
86
116
|
}
|
|
87
117
|
catch (error) {
|
|
88
118
|
if (error.code !== 'ENOENT')
|
|
@@ -93,6 +123,7 @@ export default class Push extends PushCommand {
|
|
|
93
123
|
id,
|
|
94
124
|
name: name ?? file.slice(0, file.lastIndexOf('.')),
|
|
95
125
|
path: filePath,
|
|
126
|
+
type: type ?? TYPE_BY_EXTENSION[ext] ?? 'php',
|
|
96
127
|
});
|
|
97
128
|
}
|
|
98
129
|
}
|
|
@@ -110,18 +141,19 @@ export default class Push extends PushCommand {
|
|
|
110
141
|
}
|
|
111
142
|
try {
|
|
112
143
|
const endpoint = adapter.endpoint(url);
|
|
113
|
-
const payload = adapter.toPayload(snippet.name, snippet.code, snippet.path);
|
|
144
|
+
const payload = adapter.toPayload(snippet.name, snippet.code, snippet.path, snippet.type);
|
|
114
145
|
if (snippet.id) {
|
|
115
146
|
this.log(`🔄 Updating snippet by id (${snippet.id}): ${snippet.name}`);
|
|
116
147
|
await got.put(`${endpoint}/${snippet.id}`, { headers, json: payload });
|
|
117
148
|
this.log(`✅ Updated: ${snippet.name}`);
|
|
149
|
+
await this.ensureCanonicalFilename(snippet, snippet.id, snippet.name);
|
|
118
150
|
return;
|
|
119
151
|
}
|
|
120
152
|
this.log(`➕ Creating new snippet: ${snippet.name}`);
|
|
121
153
|
const response = await got.post(endpoint, { headers, json: payload }).json();
|
|
122
154
|
const created = adapter.fromRemote(response);
|
|
123
|
-
await this.injectIdIntoMeta(snippet.path, created.id);
|
|
124
155
|
this.log(`✅ Created: ${snippet.name} (id: ${created.id})`);
|
|
156
|
+
await this.ensureCanonicalFilename(snippet, created.id, created.name);
|
|
125
157
|
}
|
|
126
158
|
catch (error) {
|
|
127
159
|
this.error(`❌ Error pushing snippet ${snippet.name}: ${error.message}`);
|
|
@@ -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
|
}
|