@loopress/cli 0.1.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.
Files changed (52) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +676 -0
  3. package/bin/dev.cmd +3 -0
  4. package/bin/dev.js +5 -0
  5. package/bin/run.cmd +3 -0
  6. package/bin/run.js +5 -0
  7. package/dist/commands/base.d.ts +14 -0
  8. package/dist/commands/base.js +49 -0
  9. package/dist/commands/login.d.ts +8 -0
  10. package/dist/commands/login.js +106 -0
  11. package/dist/commands/logout.d.ts +6 -0
  12. package/dist/commands/logout.js +15 -0
  13. package/dist/commands/project/config.d.ts +6 -0
  14. package/dist/commands/project/config.js +92 -0
  15. package/dist/commands/project/list.d.ts +6 -0
  16. package/dist/commands/project/list.js +31 -0
  17. package/dist/commands/project/remove-env.d.ts +6 -0
  18. package/dist/commands/project/remove-env.js +33 -0
  19. package/dist/commands/project/remove.d.ts +6 -0
  20. package/dist/commands/project/remove.js +34 -0
  21. package/dist/commands/project/switch-env.d.ts +6 -0
  22. package/dist/commands/project/switch-env.js +33 -0
  23. package/dist/commands/project/switch.d.ts +6 -0
  24. package/dist/commands/project/switch.js +34 -0
  25. package/dist/commands/snippets/list.d.ts +13 -0
  26. package/dist/commands/snippets/list.js +59 -0
  27. package/dist/commands/snippets/pull.d.ts +16 -0
  28. package/dist/commands/snippets/pull.js +58 -0
  29. package/dist/commands/snippets/push.d.ts +18 -0
  30. package/dist/commands/snippets/push.js +97 -0
  31. package/dist/commands/styles/pull.d.ts +12 -0
  32. package/dist/commands/styles/pull.js +52 -0
  33. package/dist/commands/styles/push.d.ts +13 -0
  34. package/dist/commands/styles/push.js +78 -0
  35. package/dist/config/auth.manager.d.ts +16 -0
  36. package/dist/config/auth.manager.js +45 -0
  37. package/dist/config/project-config.manager.d.ts +28 -0
  38. package/dist/config/project-config.manager.js +130 -0
  39. package/dist/config/types.d.ts +16 -0
  40. package/dist/config/types.js +1 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +1 -0
  43. package/dist/types/menu.d.ts +7 -0
  44. package/dist/types/menu.js +1 -0
  45. package/dist/types/snippet.d.ts +6 -0
  46. package/dist/types/snippet.js +1 -0
  47. package/dist/utils/loopress-config.d.ts +6 -0
  48. package/dist/utils/loopress-config.js +14 -0
  49. package/dist/utils/snippet-plugin.d.ts +15 -0
  50. package/dist/utils/snippet-plugin.js +58 -0
  51. package/oclif.manifest.json +578 -0
  52. package/package.json +85 -0
@@ -0,0 +1,49 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { join } from 'node:path';
3
+ import { configManager } from '../config/project-config.manager.js';
4
+ import { readLocalConfig } from '../utils/loopress-config.js';
5
+ export class LoopressCommand extends Command {
6
+ static baseFlags = {
7
+ password: Flags.string({
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
+ }),
19
+ };
20
+ siteConfig;
21
+ async buildAuthHeaders() {
22
+ const { token, url } = this.siteConfig;
23
+ if (token) {
24
+ return { Authorization: `Basic ${Buffer.from(token).toString('base64')}` };
25
+ }
26
+ this.error(`No credentials configured for ${url}. Run \`lps project config\` to add them.`);
27
+ }
28
+ async init() {
29
+ await super.init();
30
+ const env = configManager.getCurrentEnv();
31
+ if (env) {
32
+ this.siteConfig = env;
33
+ return;
34
+ }
35
+ this.error('No environment configured. Run `lps project config` first.');
36
+ }
37
+ async resolveSnippetsPath(override) {
38
+ if (override)
39
+ return override;
40
+ const config = await readLocalConfig();
41
+ return join(config.rootDir ?? '.', config.snippets ?? 'snippets');
42
+ }
43
+ async resolveStylesPath(override) {
44
+ if (override)
45
+ return override;
46
+ const config = await readLocalConfig();
47
+ return join(config.rootDir ?? '.', config.styles ?? 'styles');
48
+ }
49
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Login extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ private openBrowser;
7
+ private waitForCallback;
8
+ }
@@ -0,0 +1,106 @@
1
+ import { Command } from '@oclif/core';
2
+ import { exec } from 'node:child_process';
3
+ import { createServer } from 'node:http';
4
+ import { authManager } from '../config/auth.manager.js';
5
+ const CONSOLE_URL = 'https://console.loopress.dev';
6
+ const TIMEOUT_MS = 5 * 60 * 1000;
7
+ export default class Login extends Command {
8
+ static description = 'Log in to Loopress via the console';
9
+ static examples = ['$ lps login'];
10
+ async run() {
11
+ const { email, token } = await this.waitForCallback();
12
+ authManager.setAuth({ email, savedAt: new Date().toISOString(), token });
13
+ this.log(`\n✅ Logged in${email ? ` as ${email}` : ''}. You're all set!`);
14
+ }
15
+ openBrowser(url) {
16
+ const cmds = {
17
+ darwin: `open "${url}"`,
18
+ linux: `xdg-open "${url}"`,
19
+ win32: `start "" "${url}"`,
20
+ };
21
+ const cmd = cmds[process.platform];
22
+ if (cmd)
23
+ exec(cmd);
24
+ }
25
+ waitForCallback() {
26
+ return new Promise((resolve, reject) => {
27
+ const server = createServer((req, res) => {
28
+ try {
29
+ const url = new URL(req.url ?? '/', 'http://localhost');
30
+ const token = url.searchParams.get('token');
31
+ const email = url.searchParams.get('email') ?? undefined;
32
+ if (!token) {
33
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
34
+ res.end('Missing token');
35
+ return;
36
+ }
37
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
38
+ res.end(SUCCESS_PAGE);
39
+ clearTimeout(timer);
40
+ server.close();
41
+ resolve({ email, token });
42
+ }
43
+ catch (error) {
44
+ res.writeHead(500);
45
+ res.end('Internal error');
46
+ server.close();
47
+ reject(error);
48
+ }
49
+ });
50
+ server.on('error', (err) => {
51
+ clearTimeout(timer);
52
+ reject(err);
53
+ });
54
+ const timer = setTimeout(() => {
55
+ server.close();
56
+ reject(new Error('Login timed out after 5 minutes. Please try again.'));
57
+ }, TIMEOUT_MS);
58
+ server.listen(0, '127.0.0.1', () => {
59
+ const { port } = server.address();
60
+ const callbackUrl = `http://localhost:${port}/callback`;
61
+ const loginUrl = `${CONSOLE_URL}/cli-auth?callbackUrl=${encodeURIComponent(callbackUrl)}`;
62
+ this.log('Opening Loopress console in your browser...');
63
+ this.log(`\nIf it doesn't open automatically, visit:\n${loginUrl}\n`);
64
+ this.openBrowser(loginUrl);
65
+ });
66
+ });
67
+ }
68
+ }
69
+ const SUCCESS_PAGE = `<!DOCTYPE html>
70
+ <html lang="en">
71
+ <head>
72
+ <meta charset="UTF-8">
73
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
74
+ <title>Loopress: Logged in</title>
75
+ <style>
76
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
77
+ body {
78
+ font-family: system-ui, -apple-system, sans-serif;
79
+ background: #f0fdf4;
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ min-height: 100dvh;
84
+ }
85
+ .card {
86
+ background: #fff;
87
+ border-radius: 16px;
88
+ padding: 2.5rem 3rem;
89
+ text-align: center;
90
+ box-shadow: 0 4px 32px rgba(0, 0, 0, .08);
91
+ max-width: 420px;
92
+ width: 90%;
93
+ }
94
+ .icon { font-size: 3rem; margin-bottom: 1rem; }
95
+ h1 { color: #15803d; font-size: 1.5rem; margin-bottom: .5rem; }
96
+ p { color: #6b7280; font-size: .95rem; line-height: 1.5; }
97
+ </style>
98
+ </head>
99
+ <body>
100
+ <div class="card">
101
+ <div class="icon">✅</div>
102
+ <h1>Login successful!</h1>
103
+ <p>You can close this tab and return to your terminal.</p>
104
+ </div>
105
+ </body>
106
+ </html>`;
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Logout extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ import { authManager } from '../config/auth.manager.js';
3
+ export default class Logout extends Command {
4
+ static description = 'Log out from Loopress console';
5
+ static examples = ['$ lps logout'];
6
+ async run() {
7
+ const auth = authManager.getAuth();
8
+ if (!auth) {
9
+ this.log('You are not logged in.');
10
+ return;
11
+ }
12
+ authManager.clearAuth();
13
+ this.log(`✅ Logged out${auth.email ? ` (${auth.email})` : ''}.`);
14
+ }
15
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Config extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,92 @@
1
+ import { confirm, input, password as passwordPrompt, select } from '@inquirer/prompts';
2
+ import { Command } from '@oclif/core';
3
+ import { configManager } from '../../config/project-config.manager.js';
4
+ export default class Config extends Command {
5
+ static description = 'Add or update a WordPress project environment';
6
+ static examples = ['$ lps project config'];
7
+ async run() {
8
+ await this.parse(Config);
9
+ const projectName = await input({
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
+ });
18
+ const envChoice = await select({
19
+ choices: [
20
+ { name: 'production', value: 'production' },
21
+ { name: 'staging', value: 'staging' },
22
+ { name: 'development', value: 'development' },
23
+ { name: 'Custom…', value: '__custom__' },
24
+ ],
25
+ message: 'Environment',
26
+ });
27
+ const envName = envChoice === '__custom__'
28
+ ? await input({
29
+ message: 'Environment name',
30
+ validate: (value) => (value.trim().length > 0 ? true : 'Name cannot be empty'),
31
+ })
32
+ : envChoice;
33
+ const existingProject = configManager.getProject(projectName);
34
+ const existingEnv = existingProject?.environments[envName];
35
+ if (existingEnv) {
36
+ const overwrite = await confirm({
37
+ default: false,
38
+ message: `"${projectName}/${envName}" already exists. Overwrite?`,
39
+ });
40
+ if (!overwrite) {
41
+ this.log('Aborted.');
42
+ return;
43
+ }
44
+ }
45
+ const url = await input({
46
+ message: 'WordPress URL',
47
+ validate(value) {
48
+ try {
49
+ const parsed = new URL(value);
50
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
51
+ return 'URL must start with http:// or https://';
52
+ }
53
+ return true;
54
+ }
55
+ catch {
56
+ return 'Invalid URL';
57
+ }
58
+ },
59
+ });
60
+ const user = await input({
61
+ message: 'Username',
62
+ validate: (value) => (value.trim().length > 0 ? true : 'Username cannot be empty'),
63
+ });
64
+ const appPassword = await passwordPrompt({
65
+ mask: '*',
66
+ message: 'Application password',
67
+ validate: (value) => (value.trim().length > 0 ? true : 'Application password cannot be empty'),
68
+ });
69
+ const token = `${user}:${appPassword}`;
70
+ const env = {
71
+ addedAt: new Date().toISOString(),
72
+ name: envName,
73
+ token,
74
+ url,
75
+ };
76
+ if (existingProject) {
77
+ configManager.setEnvironment(projectName, envName, env);
78
+ }
79
+ else {
80
+ const project = {
81
+ addedAt: new Date().toISOString(),
82
+ currentEnv: envName,
83
+ environments: { [envName]: env },
84
+ name: projectName,
85
+ };
86
+ configManager.setProject(projectName, project);
87
+ }
88
+ this.log(`✓ "${projectName}/${envName}" configured`);
89
+ this.log('→ Run `lps project switch` to change active project');
90
+ this.log('→ Run `lps project switch-env` to change active environment');
91
+ }
92
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class List extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,31 @@
1
+ import { Command, ux } from '@oclif/core';
2
+ import { configManager } from '../../config/project-config.manager.js';
3
+ const c = ux.colorize;
4
+ export default class List extends Command {
5
+ static description = 'List configured WordPress projects';
6
+ static examples = ['$ lps project list'];
7
+ async run() {
8
+ await this.parse(List);
9
+ const projects = configManager.listProjects();
10
+ if (projects.length === 0) {
11
+ this.log('No projects configured. Run `lps project config` first.');
12
+ return;
13
+ }
14
+ for (const project of projects) {
15
+ const envs = Object.values(project.environments);
16
+ const marker = project.isCurrent ? c('green', '●') : c('dim', '○');
17
+ const name = project.isCurrent ? c('green', project.name) : project.name;
18
+ const currentTag = project.isCurrent ? ` ${c('green', '[current]')}` : '';
19
+ this.log(`${marker} ${name}${currentTag}`);
20
+ for (const env of envs) {
21
+ const isActiveEnv = env.name === project.currentEnv;
22
+ const envMarker = isActiveEnv ? c('cyan', '·') : c('dim', '·');
23
+ const envName = isActiveEnv ? c('cyan', env.name.padEnd(15)) : c('dim', env.name.padEnd(15));
24
+ const envUrl = c('dim', env.url);
25
+ const activeTag = isActiveEnv ? ` ${c('cyan', '←')}` : '';
26
+ this.log(` ${envMarker} ${envName} ${envUrl}${activeTag}`);
27
+ }
28
+ this.log('');
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class RemoveEnv extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,33 @@
1
+ import { checkbox } from '@inquirer/prompts';
2
+ import { Command } from '@oclif/core';
3
+ import { configManager } from '../../config/project-config.manager.js';
4
+ export default class RemoveEnv extends Command {
5
+ static description = 'Remove one or more environments from the current project';
6
+ static examples = ['$ lps project remove-env'];
7
+ async run() {
8
+ await this.parse(RemoveEnv);
9
+ const project = configManager.getCurrentProject();
10
+ if (!project) {
11
+ this.error('No project configured. Run `lps project config` first.');
12
+ }
13
+ const envs = configManager.listEnvironments(project.name);
14
+ if (envs.length === 0) {
15
+ this.error('No environments configured.');
16
+ }
17
+ const chosen = await checkbox({
18
+ choices: envs.map((env) => ({
19
+ name: `${env.isCurrent ? '●' : '○'} ${env.name.padEnd(20)} ${env.url}${env.isCurrent ? ' [current]' : ''}`,
20
+ value: env.name,
21
+ })),
22
+ message: 'Select environments to remove',
23
+ });
24
+ if (chosen.length === 0) {
25
+ this.log('Nothing removed.');
26
+ return;
27
+ }
28
+ for (const envName of chosen) {
29
+ configManager.removeEnvironment(project.name, envName);
30
+ }
31
+ this.log(`✓ Removed: ${chosen.join(', ')}`);
32
+ }
33
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Remove extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,34 @@
1
+ import { checkbox } from '@inquirer/prompts';
2
+ import { Command } from '@oclif/core';
3
+ import { configManager } from '../../config/project-config.manager.js';
4
+ export default class Remove extends Command {
5
+ static description = 'Remove one or more WordPress project configurations';
6
+ static examples = ['$ lps project remove'];
7
+ async run() {
8
+ await this.parse(Remove);
9
+ const projects = configManager.listProjects();
10
+ if (projects.length === 0) {
11
+ this.error('No projects configured.');
12
+ }
13
+ const chosen = await checkbox({
14
+ choices: projects.map((project) => {
15
+ const envCount = Object.keys(project.environments).length;
16
+ const envLabel = `${envCount} env${envCount > 1 ? 's' : ''}`;
17
+ const currentMarker = project.isCurrent ? ' [current]' : '';
18
+ return {
19
+ name: `${project.isCurrent ? '●' : '○'} ${project.name.padEnd(20)} (${envLabel})${currentMarker}`,
20
+ value: project.name,
21
+ };
22
+ }),
23
+ message: 'Select projects to remove',
24
+ });
25
+ if (chosen.length === 0) {
26
+ this.log('Nothing removed.');
27
+ return;
28
+ }
29
+ for (const name of chosen) {
30
+ configManager.removeProject(name);
31
+ }
32
+ this.log(`✓ Removed: ${chosen.join(', ')}`);
33
+ }
34
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SwitchEnv extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,33 @@
1
+ import { select } from '@inquirer/prompts';
2
+ import { Command } from '@oclif/core';
3
+ import { configManager } from '../../config/project-config.manager.js';
4
+ export default class SwitchEnv extends Command {
5
+ static description = 'Switch the active environment within the current project';
6
+ static examples = ['$ lps project switch-env'];
7
+ async run() {
8
+ await this.parse(SwitchEnv);
9
+ const project = configManager.getCurrentProject();
10
+ if (!project) {
11
+ this.error('No project configured. Run `lps project config` first.');
12
+ }
13
+ const envs = configManager.listEnvironments(project.name);
14
+ if (envs.length === 0) {
15
+ this.error('No environments configured for this project.');
16
+ }
17
+ if (envs.length === 1) {
18
+ configManager.setCurrentEnv(project.name, envs[0].name);
19
+ this.log(`✓ Switched to "${project.name}/${envs[0].name}"`);
20
+ return;
21
+ }
22
+ const chosen = await select({
23
+ choices: envs.map((env) => ({
24
+ name: `${env.isCurrent ? '●' : '○'} ${env.name.padEnd(20)} ${env.url}${env.isCurrent ? ' [current]' : ''}`,
25
+ value: env.name,
26
+ })),
27
+ default: envs.find((e) => e.isCurrent)?.name,
28
+ message: `Select environment for "${project.name}"`,
29
+ });
30
+ configManager.setCurrentEnv(project.name, chosen);
31
+ this.log(`✓ Switched to "${project.name}/${chosen}"`);
32
+ }
33
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Switch extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,34 @@
1
+ import { select } from '@inquirer/prompts';
2
+ import { Command } from '@oclif/core';
3
+ import { configManager } from '../../config/project-config.manager.js';
4
+ export default class Switch extends Command {
5
+ static description = 'Switch the active project';
6
+ static examples = ['$ lps project switch'];
7
+ async run() {
8
+ await this.parse(Switch);
9
+ const projects = configManager.listProjects();
10
+ if (projects.length === 0) {
11
+ this.error('No projects configured. Run `lps project config` first.');
12
+ }
13
+ if (projects.length === 1) {
14
+ configManager.setCurrentProject(projects[0].name);
15
+ this.log(`✓ Switched to "${projects[0].name}"`);
16
+ return;
17
+ }
18
+ const chosen = await select({
19
+ choices: projects.map((project) => {
20
+ const envCount = Object.keys(project.environments).length;
21
+ const envLabel = `${envCount} env${envCount > 1 ? 's' : ''}`;
22
+ const currentMarker = project.isCurrent ? ' [current]' : '';
23
+ return {
24
+ name: `${project.isCurrent ? '●' : '○'} ${project.name.padEnd(20)} (${envLabel})${currentMarker}`,
25
+ value: project.name,
26
+ };
27
+ }),
28
+ default: projects.find((p) => p.isCurrent)?.name,
29
+ message: 'Select active project',
30
+ });
31
+ configManager.setCurrentProject(chosen);
32
+ this.log(`✓ Switched to "${chosen}"`);
33
+ }
34
+ }
@@ -0,0 +1,13 @@
1
+ import { LoopressCommand } from '../base.js';
2
+ export default class List extends LoopressCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ plugin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
+ password: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,59 @@
1
+ import { Flags } from '@oclif/core';
2
+ import got from 'got';
3
+ import { getSnippetPlugin } from '../../utils/snippet-plugin.js';
4
+ import { LoopressCommand } from '../base.js';
5
+ export default class List extends LoopressCommand {
6
+ static description = 'List snippets from WordPress';
7
+ static examples = [
8
+ '$ lps snippets list',
9
+ '$ lps snippets list --url http://example.com',
10
+ '$ lps snippets list --plugin wpcode',
11
+ ];
12
+ static flags = {
13
+ ...LoopressCommand.baseFlags,
14
+ json: Flags.boolean({ char: 'j', description: 'Output in JSON format' }),
15
+ plugin: Flags.string({
16
+ char: 'p',
17
+ default: 'code-snippets',
18
+ description: 'WordPress snippet plugin to target',
19
+ options: ['code-snippets', 'wpcode'],
20
+ }),
21
+ };
22
+ async run() {
23
+ const { flags } = await this.parse(List);
24
+ const { json, plugin } = flags;
25
+ const { url } = this.siteConfig;
26
+ try {
27
+ const adapter = getSnippetPlugin(plugin);
28
+ const endpoint = adapter.endpoint(url);
29
+ const headers = await this.buildAuthHeaders();
30
+ const remoteList = await got.get(endpoint, { headers }).json();
31
+ const snippets = remoteList.map((r) => adapter.fromRemote(r));
32
+ if (json) {
33
+ this.log(JSON.stringify(snippets, null, 2));
34
+ }
35
+ else {
36
+ if (snippets.length === 0) {
37
+ this.log('No snippets found');
38
+ return;
39
+ }
40
+ this.log(`Found ${snippets.length} snippet${snippets.length === 1 ? '' : 's'}:`);
41
+ console.log('');
42
+ for (const snippet of snippets) {
43
+ this.log(` ${snippet.id}. ${snippet.name}`);
44
+ this.log(` Active: ${snippet.active ? '✓' : '✗'}`);
45
+ if (snippet.tags && snippet.tags.length > 0) {
46
+ this.log(` Tags: ${snippet.tags.join(', ')}`);
47
+ }
48
+ if (snippet.description) {
49
+ this.log(` Description: ${snippet.description}`);
50
+ }
51
+ console.log('');
52
+ }
53
+ }
54
+ }
55
+ catch (error) {
56
+ this.error(`❌ Error listing snippets: ${error.message}`);
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,16 @@
1
+ import { LoopressCommand } from '../base.js';
2
+ export default class Pull extends LoopressCommand {
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
+ static flags: {
9
+ dryRun: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ plugin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ password: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ };
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,58 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import got from 'got';
3
+ import { getSnippetPlugin } from '../../utils/snippet-plugin.js';
4
+ import { LoopressCommand } from '../base.js';
5
+ export default class Pull extends LoopressCommand {
6
+ static args = {
7
+ path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
8
+ };
9
+ static description = 'Pull snippets from WordPress';
10
+ static examples = [
11
+ '$ lps snippets pull',
12
+ '$ lps snippets pull --url http://example.com',
13
+ '$ lps snippets pull --path ./snippets',
14
+ '$ lps snippets pull --plugin wpcode',
15
+ ];
16
+ static flags = {
17
+ ...LoopressCommand.baseFlags,
18
+ dryRun: Flags.boolean({ char: 'd', description: 'Dry run - show what would happen without making changes' }),
19
+ plugin: Flags.string({
20
+ char: 'p',
21
+ default: 'code-snippets',
22
+ description: 'WordPress snippet plugin to target',
23
+ options: ['code-snippets', 'wpcode'],
24
+ }),
25
+ };
26
+ async run() {
27
+ const { args, flags } = await this.parse(Pull);
28
+ const { dryRun, plugin } = flags;
29
+ const { url } = this.siteConfig;
30
+ const path = await this.resolveSnippetsPath(args.path);
31
+ this.log(`📥 Pulling snippets from ${url} via ${plugin}`);
32
+ this.log(`📂 From snippet path: ${path}`);
33
+ this.log(`🔄 Dry run: ${dryRun ? 'yes' : 'no'}`);
34
+ try {
35
+ const adapter = getSnippetPlugin(plugin);
36
+ const endpoint = adapter.endpoint(url);
37
+ const headers = await this.buildAuthHeaders();
38
+ const remoteList = await got.get(endpoint, { headers }).json();
39
+ const snippets = remoteList.map((r) => adapter.fromRemote(r));
40
+ const fs = await import('node:fs/promises');
41
+ if (dryRun) {
42
+ this.log(`📝 [DRY RUN] Would pull ${snippets.length} snippets`);
43
+ return;
44
+ }
45
+ let count = 0;
46
+ for (const snippet of snippets) {
47
+ const filePath = `${path}/${snippet.name}.php`;
48
+ await fs.writeFile(filePath, snippet.code);
49
+ count++;
50
+ this.log(`✅ Pulled: ${snippet.name}`);
51
+ }
52
+ this.log(`🎉 Successfully pulled ${count} snippet${count === 1 ? '' : 's'} to ${path}`);
53
+ }
54
+ catch (error) {
55
+ this.error(`❌ Error pulling snippets: ${error.message}`);
56
+ }
57
+ }
58
+ }