@loopress/cli 0.2.0 → 0.4.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.
@@ -0,0 +1,111 @@
1
+ import { confirm } from '@inquirer/prompts';
2
+ import { Flags } from '@oclif/core';
3
+ import got from 'got';
4
+ import { LoopressCommand } from '../../lib/base.js';
5
+ import { readLocalConfig } from '../../utils/loopress-config.js';
6
+ import { diffPlugins } from '../../utils/plugins.js';
7
+ export default class Push extends LoopressCommand {
8
+ static description = 'Sync plugins on WordPress to match loopress.json';
9
+ static examples = ['$ lps plugins push', '$ lps plugins push --dry-run'];
10
+ static flags = {
11
+ ...LoopressCommand.baseFlags,
12
+ 'dry-run': Flags.boolean({ char: 'd', description: 'Show what would change without making changes' }),
13
+ };
14
+ async run() {
15
+ const { flags } = await this.parse(Push);
16
+ const dryRun = flags['dry-run'];
17
+ const { url } = this.siteConfig;
18
+ const localConfig = await readLocalConfig();
19
+ const manifest = localConfig.plugins;
20
+ if (!manifest || Object.keys(manifest).length === 0) {
21
+ this.error('No plugins found in loopress.json. Run `lps plugins pull` first.');
22
+ }
23
+ this.log(`Pushing plugins to ${url}`);
24
+ const headers = await this.buildAuthHeaders();
25
+ const installed = await got.get(`${url}/wp-json/loopress/v1/plugins`, { headers }).json();
26
+ const { drifted, toActivate, toInstall } = diffPlugins(manifest, installed);
27
+ if (toInstall.length === 0 && toActivate.length === 0 && drifted.length === 0) {
28
+ this.log('Everything is already in sync.');
29
+ return;
30
+ }
31
+ if (toInstall.length > 0) {
32
+ this.log(`\nTo install (${toInstall.length}):`);
33
+ for (const a of toInstall)
34
+ this.log(` + ${a.slug} @ ${a.targetVersion}`);
35
+ }
36
+ if (toActivate.length > 0) {
37
+ this.log(`\nTo activate (${toActivate.length}):`);
38
+ for (const a of toActivate)
39
+ this.log(` ↑ ${a.slug}`);
40
+ }
41
+ if (drifted.length > 0) {
42
+ this.log(`\nVersion mismatch (${drifted.length}):`);
43
+ for (const a of drifted) {
44
+ this.log(` ~ ${a.slug}: site has ${a.currentVersion}, manifest wants ${a.targetVersion}`);
45
+ }
46
+ }
47
+ if (dryRun)
48
+ return;
49
+ // Install missing plugins and activate them.
50
+ for (const action of toInstall) {
51
+ this.log(`\nInstalling ${action.slug} @ ${action.targetVersion}...`);
52
+ try {
53
+ const result = await got
54
+ .post(`${url}/wp-json/loopress/v1/plugins/install`, {
55
+ headers,
56
+ json: { slug: action.slug, version: action.targetVersion },
57
+ })
58
+ .json();
59
+ this.log(` ✓ ${result.message}`);
60
+ }
61
+ catch (error) {
62
+ this.warn(` Failed to install ${action.slug}: ${error.message}`);
63
+ continue;
64
+ }
65
+ await this.activatePlugin(url, headers, action.slug);
66
+ }
67
+ // Activate installed-but-inactive plugins without prompting.
68
+ for (const action of toActivate) {
69
+ this.log(`\nActivating ${action.slug}...`);
70
+ await this.activatePlugin(url, headers, action.slug);
71
+ }
72
+ // Prompt per drifted plugin before syncing.
73
+ for (const action of drifted) {
74
+ this.log('');
75
+ const proceed = await confirm({
76
+ default: false,
77
+ message: `${action.slug} is at ${action.currentVersion} on the site but manifest wants ${action.targetVersion}. Sync to ${action.targetVersion}?`,
78
+ });
79
+ if (!proceed) {
80
+ this.log(` Skipped ${action.slug}`);
81
+ continue;
82
+ }
83
+ this.log(` Syncing ${action.slug} to ${action.targetVersion}...`);
84
+ try {
85
+ const result = await got
86
+ .post(`${url}/wp-json/loopress/v1/plugins/install`, {
87
+ headers,
88
+ json: { slug: action.slug, version: action.targetVersion },
89
+ })
90
+ .json();
91
+ this.log(` ✓ ${result.message}`);
92
+ }
93
+ catch (error) {
94
+ this.warn(` Failed to sync ${action.slug}: ${error.message}`);
95
+ continue;
96
+ }
97
+ await this.activatePlugin(url, headers, action.slug);
98
+ }
99
+ }
100
+ async activatePlugin(url, headers, slug) {
101
+ try {
102
+ const result = await got
103
+ .post(`${url}/wp-json/loopress/v1/plugins/activate`, { headers, json: { slug } })
104
+ .json();
105
+ this.log(` ✓ ${result.message}`);
106
+ }
107
+ catch (error) {
108
+ this.warn(` Failed to activate ${slug}: ${error.message}`);
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,17 @@
1
+ import { LoopressCommand } from '../../lib/base.js';
2
+ export declare function resolvePluginVersion(slug: string, version: string): Promise<string>;
3
+ export default class Require extends LoopressCommand {
4
+ static args: {
5
+ slug: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ version: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
+ };
8
+ static description: string;
9
+ static examples: string[];
10
+ static flags: {
11
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ password: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
@@ -0,0 +1,74 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import got from 'got';
3
+ import { LoopressCommand } from '../../lib/base.js';
4
+ import { readLocalConfig, writeLocalConfig } from '../../utils/loopress-config.js';
5
+ const WP_ORG_API = 'https://api.wordpress.org/plugins/info/1.2/';
6
+ export async function resolvePluginVersion(slug, version) {
7
+ if (version !== 'latest')
8
+ return version;
9
+ let info;
10
+ try {
11
+ info = await got
12
+ .get(WP_ORG_API, {
13
+ searchParams: {
14
+ action: 'plugin_information',
15
+ 'request[slug]': slug,
16
+ },
17
+ })
18
+ .json();
19
+ }
20
+ catch {
21
+ throw new Error(`Plugin "${slug}" not found on WordPress.org.`);
22
+ }
23
+ if (info.error)
24
+ throw new Error(`Plugin "${slug}" not found on WordPress.org.`);
25
+ return info.version;
26
+ }
27
+ export default class Require extends LoopressCommand {
28
+ static args = {
29
+ slug: Args.string({ description: 'Plugin slug (WordPress.org)', required: true }),
30
+ version: Args.string({ description: 'Version to pin (default: latest)' }),
31
+ };
32
+ static description = 'Add a plugin to loopress.json, resolving its latest version from WordPress.org';
33
+ static examples = [
34
+ '$ lps plugins require woocommerce',
35
+ '$ lps plugins require woocommerce 8.9.1',
36
+ '$ lps plugins require contact-form-7 --dry-run',
37
+ ];
38
+ static flags = {
39
+ ...LoopressCommand.baseFlags,
40
+ 'dry-run': Flags.boolean({ char: 'd', description: 'Show what would be written without making changes' }),
41
+ };
42
+ async run() {
43
+ const { args, flags } = await this.parse(Require);
44
+ const dryRun = flags['dry-run'];
45
+ const { slug } = args;
46
+ const requestedVersion = args.version ?? 'latest';
47
+ this.log(`Resolving ${slug}@${requestedVersion}...`);
48
+ let resolvedVersion;
49
+ try {
50
+ resolvedVersion = await resolvePluginVersion(slug, requestedVersion);
51
+ }
52
+ catch (error) {
53
+ this.error(error.message);
54
+ }
55
+ this.log(`Resolved: ${slug}@${resolvedVersion}`);
56
+ const localConfig = await readLocalConfig();
57
+ const existing = localConfig.plugins ?? {};
58
+ if (existing[slug] === resolvedVersion) {
59
+ this.log(`${slug}@${resolvedVersion} is already in loopress.json — nothing to do.`);
60
+ return;
61
+ }
62
+ const updated = existing[slug] !== undefined;
63
+ const label = updated ? `${existing[slug]} → ${resolvedVersion}` : resolvedVersion;
64
+ if (dryRun) {
65
+ this.log(`[dry-run] Would ${updated ? 'update' : 'add'} ${slug}: ${label}`);
66
+ return;
67
+ }
68
+ await writeLocalConfig({
69
+ ...localConfig,
70
+ plugins: { ...existing, [slug]: resolvedVersion },
71
+ });
72
+ this.log(`${updated ? 'Updated' : 'Added'} ${slug}: ${label}`);
73
+ }
74
+ }
@@ -13,6 +13,7 @@ const EXTENSIONS = {
13
13
  const sanitize = (value) => value.replaceAll(/\s*\n\s*/g, ' ').trim();
14
14
  function buildMetaLines(snippet) {
15
15
  return [
16
+ `id: ${snippet.id}`,
16
17
  `name: ${sanitize(snippet.name)}`,
17
18
  ...(snippet.description ? [`description: ${sanitize(snippet.description)}`] : []),
18
19
  `type: ${snippet.type}`,
@@ -13,6 +13,8 @@ export default class Push extends LoopressCommand {
13
13
  user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
14
  };
15
15
  run(): Promise<void>;
16
+ private injectIdIntoFile;
16
17
  private loadSnippets;
18
+ private parseMetaFromContent;
17
19
  private pushSnippet;
18
20
  }
@@ -45,6 +45,20 @@ export default class Push extends LoopressCommand {
45
45
  this.error(error.message);
46
46
  }
47
47
  }
48
+ async injectIdIntoFile(filePath, content, id) {
49
+ const fs = await import('node:fs/promises');
50
+ let updated;
51
+ if (content.includes('/**')) {
52
+ updated = content.replace('/**', `/**\n * id: ${id}`);
53
+ }
54
+ else if (content.includes('<!--')) {
55
+ updated = content.replace('<!--', `<!--\n id: ${id}`);
56
+ }
57
+ else {
58
+ return;
59
+ }
60
+ await fs.writeFile(filePath, updated);
61
+ }
48
62
  async loadSnippets(path) {
49
63
  const fs = await import('node:fs/promises');
50
64
  const snippets = [];
@@ -54,9 +68,11 @@ export default class Push extends LoopressCommand {
54
68
  if (file.endsWith('.php')) {
55
69
  const filePath = `${path}/${file}`;
56
70
  const content = await fs.readFile(filePath, 'utf8');
71
+ const meta = this.parseMetaFromContent(content);
57
72
  snippets.push({
58
73
  code: content,
59
- name: file.replace('.php', ''),
74
+ id: meta.id,
75
+ name: meta.name ?? file.replace('.php', ''),
60
76
  path: filePath,
61
77
  });
62
78
  }
@@ -67,6 +83,14 @@ export default class Push extends LoopressCommand {
67
83
  }
68
84
  return snippets;
69
85
  }
86
+ parseMetaFromContent(content) {
87
+ const idMatch = content.match(/[\s*]*id:\s*(\d+)/);
88
+ const nameMatch = content.match(/[\s*]*name:\s*(.+)/);
89
+ return {
90
+ id: idMatch ? Number(idMatch[1]) : undefined,
91
+ name: nameMatch ? nameMatch[1].trim() : undefined,
92
+ };
93
+ }
70
94
  async pushSnippet(snippet, url, headers, dryRun, adapter) {
71
95
  if (dryRun) {
72
96
  this.log(`📝 [DRY RUN] Would push snippet: ${snippet.name}`);
@@ -75,20 +99,18 @@ export default class Push extends LoopressCommand {
75
99
  }
76
100
  try {
77
101
  const endpoint = adapter.endpoint(url);
78
- const remoteList = await got.get(endpoint, { headers }).json();
79
- const existing = remoteList
80
- .map((r) => adapter.fromRemote(r))
81
- .find((s) => s.name === snippet.name);
82
102
  const payload = adapter.toPayload(snippet.name, snippet.code, snippet.path);
83
- if (existing) {
84
- this.log(`🔄 Updating existing snippet: ${snippet.name}`);
85
- await got.put(`${endpoint}/${existing.id}`, { headers, json: payload });
86
- }
87
- else {
88
- this.log(`➕ Creating new snippet: ${snippet.name}`);
89
- await got.post(endpoint, { headers, json: payload });
103
+ if (snippet.id) {
104
+ this.log(`🔄 Updating snippet by id (${snippet.id}): ${snippet.name}`);
105
+ await got.put(`${endpoint}/${snippet.id}`, { headers, json: payload });
106
+ this.log(`✅ Updated: ${snippet.name}`);
107
+ return;
90
108
  }
91
- this.log(`✅ ${existing ? 'Updated' : 'Created'}: ${snippet.name}`);
109
+ this.log(`➕ Creating new snippet: ${snippet.name}`);
110
+ const response = await got.post(endpoint, { headers, json: payload }).json();
111
+ const created = adapter.fromRemote(response);
112
+ await this.injectIdIntoFile(snippet.path, snippet.code, created.id);
113
+ this.log(`✅ Created: ${snippet.name} (id: ${created.id})`);
92
114
  }
93
115
  catch (error) {
94
116
  this.error(`❌ Error pushing snippet ${snippet.name}: ${error.message}`);
@@ -0,0 +1,15 @@
1
+ export interface InstalledPlugin {
2
+ active: boolean;
3
+ file: string;
4
+ name: string;
5
+ slug: string;
6
+ version: string;
7
+ }
8
+ export interface InstallResult {
9
+ message: string;
10
+ version: string;
11
+ }
12
+ export interface ActivateResult {
13
+ message: string;
14
+ }
15
+ export type PluginManifest = Record<string, string>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,8 @@
1
1
  export interface LoopressLocalConfig {
2
+ plugins?: Record<string, string>;
2
3
  rootDir?: string;
3
4
  snippets?: string;
4
5
  styles?: string;
5
6
  }
6
7
  export declare function readLocalConfig(): Promise<LoopressLocalConfig>;
8
+ export declare function writeLocalConfig(config: LoopressLocalConfig): Promise<void>;
@@ -1,14 +1,19 @@
1
1
  import { existsSync } from 'node:fs';
2
+ import { readFile, writeFile } from 'node:fs/promises';
2
3
  import { join } from 'node:path';
3
4
  export async function readLocalConfig() {
4
- const configPath = join(process.cwd(), 'loopress.config.js');
5
+ const configPath = join(process.cwd(), 'loopress.json');
5
6
  if (!existsSync(configPath))
6
7
  return {};
7
8
  try {
8
- const mod = await import(configPath);
9
- return mod.default ?? {};
9
+ const content = await readFile(configPath, 'utf8');
10
+ return JSON.parse(content);
10
11
  }
11
12
  catch {
12
13
  return {};
13
14
  }
14
15
  }
16
+ export async function writeLocalConfig(config) {
17
+ const configPath = join(process.cwd(), 'loopress.json');
18
+ await writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
19
+ }
@@ -0,0 +1,27 @@
1
+ import { InstalledPlugin, PluginManifest } from '../types/plugin.js';
2
+ export interface PluginDiff {
3
+ drifted: Array<{
4
+ currentVersion: string;
5
+ slug: string;
6
+ targetVersion: string;
7
+ }>;
8
+ toActivate: Array<{
9
+ slug: string;
10
+ }>;
11
+ toInstall: Array<{
12
+ slug: string;
13
+ targetVersion: string;
14
+ }>;
15
+ upToDate: string[];
16
+ }
17
+ export interface MergeResult {
18
+ added: string[];
19
+ merged: PluginManifest;
20
+ updated: Array<{
21
+ from: string;
22
+ slug: string;
23
+ to: string;
24
+ }>;
25
+ }
26
+ export declare function mergePluginManifest(existing: PluginManifest, incoming: PluginManifest): MergeResult;
27
+ export declare function diffPlugins(manifest: PluginManifest, installed: InstalledPlugin[]): PluginDiff;
@@ -0,0 +1,34 @@
1
+ export function mergePluginManifest(existing, incoming) {
2
+ const merged = { ...existing, ...incoming };
3
+ const added = Object.keys(incoming).filter((s) => !(s in existing));
4
+ const updated = Object.keys(incoming)
5
+ .filter((s) => s in existing && existing[s] !== incoming[s])
6
+ .map((s) => ({ from: existing[s], slug: s, to: incoming[s] }));
7
+ return { added, merged, updated };
8
+ }
9
+ export function diffPlugins(manifest, installed) {
10
+ const installedMap = new Map(installed.map((p) => [p.slug, p]));
11
+ const toInstall = [];
12
+ const toActivate = [];
13
+ const drifted = [];
14
+ const upToDate = [];
15
+ for (const [slug, targetVersion] of Object.entries(manifest)) {
16
+ const live = installedMap.get(slug);
17
+ if (!live) {
18
+ toInstall.push({ slug, targetVersion });
19
+ continue;
20
+ }
21
+ if (live.version === targetVersion) {
22
+ if (live.active) {
23
+ upToDate.push(slug);
24
+ }
25
+ else {
26
+ toActivate.push({ slug });
27
+ }
28
+ }
29
+ else {
30
+ drifted.push({ currentVersion: live.version, slug, targetVersion });
31
+ }
32
+ }
33
+ return { drifted, toActivate, toInstall, upToDate };
34
+ }