@loopress/cli 0.3.0 → 0.5.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 (33) hide show
  1. package/README.md +88 -334
  2. package/dist/commands/init.d.ts +6 -0
  3. package/dist/commands/init.js +73 -0
  4. package/dist/commands/{styles → plugin}/pull.d.ts +1 -1
  5. package/dist/commands/plugin/pull.js +48 -0
  6. package/dist/commands/{styles → plugin}/push.d.ts +4 -4
  7. package/dist/commands/plugin/push.js +113 -0
  8. package/dist/commands/plugin/require.d.ts +17 -0
  9. package/dist/commands/plugin/require.js +74 -0
  10. package/dist/commands/project/config.js +2 -2
  11. package/dist/commands/{snippets → snippet}/list.d.ts +1 -1
  12. package/dist/commands/{snippets → snippet}/list.js +3 -3
  13. package/dist/commands/{snippets → snippet}/pull.d.ts +4 -1
  14. package/dist/commands/{snippets → snippet}/pull.js +26 -37
  15. package/dist/commands/{snippets → snippet}/push.d.ts +4 -5
  16. package/dist/commands/{snippets → snippet}/push.js +50 -39
  17. package/dist/config/auth.manager.js +2 -2
  18. package/dist/config/project-config.manager.js +2 -2
  19. package/dist/lib/base.d.ts +1 -1
  20. package/dist/lib/base.js +18 -6
  21. package/dist/lib/push-command.d.ts +7 -0
  22. package/dist/lib/push-command.js +32 -0
  23. package/dist/types/plugin.d.ts +15 -0
  24. package/dist/types/plugin.js +1 -0
  25. package/dist/utils/loopress-config.d.ts +5 -2
  26. package/dist/utils/loopress-config.js +8 -3
  27. package/dist/utils/plugins.d.ts +27 -0
  28. package/dist/utils/plugins.js +34 -0
  29. package/dist/utils/snippet-plugin.js +1 -1
  30. package/oclif.manifest.json +217 -131
  31. package/package.json +14 -19
  32. package/dist/commands/styles/pull.js +0 -52
  33. package/dist/commands/styles/push.js +0 -78
@@ -1,8 +1,8 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import got from 'got';
3
- import { LoopressCommand } from '../../lib/base.js';
3
+ import { PushCommand } from '../../lib/push-command.js';
4
4
  import { getSnippetPlugin } from '../../utils/snippet-plugin.js';
5
- export default class Push extends LoopressCommand {
5
+ export default class Push extends PushCommand {
6
6
  static args = {
7
7
  path: Args.string({ description: 'Path to snippets directory (overrides project config)' }),
8
8
  };
@@ -14,68 +14,86 @@ export default class Push extends LoopressCommand {
14
14
  '$ lps snippets push --plugin wpcode',
15
15
  ];
16
16
  static flags = {
17
- ...LoopressCommand.baseFlags,
17
+ ...PushCommand.baseFlags,
18
18
  dryRun: Flags.boolean({ char: 'd', description: 'Dry run - show what would happen without making changes' }),
19
19
  plugin: Flags.string({
20
20
  char: 'p',
21
- default: 'code-snippets',
22
- description: 'WordPress snippet plugin to target',
21
+ description: 'WordPress snippet plugin to target (overrides loopress.json)',
23
22
  options: ['code-snippets', 'wpcode'],
24
23
  }),
25
24
  };
26
25
  async run() {
27
26
  const { args, flags } = await this.parse(Push);
28
27
  const { dryRun, plugin } = flags;
28
+ this.dryRun = dryRun;
29
29
  const { url } = this.siteConfig;
30
30
  const path = await this.resolveSnippetsPath(args.path);
31
- this.log(`🚀 Pushing snippets to ${url} via ${plugin}`);
31
+ const resolvedPlugin = await this.resolveSnippetPlugin(plugin);
32
+ this.log(`🚀 Pushing snippets to ${url} via ${resolvedPlugin}`);
32
33
  this.log(`📂 From snippet path: ${path}`);
33
34
  this.log(`🔄 Dry run: ${dryRun ? 'yes' : 'no'}`);
35
+ let snippets = [];
34
36
  try {
35
- const snippets = await this.loadSnippets(path);
37
+ snippets = await this.loadSnippets(path);
36
38
  this.log(`✅ Found ${snippets.length} snippets to push`);
37
39
  const headers = await this.buildAuthHeaders();
38
- const adapter = getSnippetPlugin(plugin);
40
+ const adapter = getSnippetPlugin(resolvedPlugin);
39
41
  for (const snippet of snippets) {
40
- await this.pushSnippet(snippet, url, headers, dryRun, adapter);
42
+ await this.pushSnippet(snippet, { adapter, dryRun, headers, url });
41
43
  }
44
+ await this.recordSuccess();
42
45
  this.log('🎉 All snippets pushed successfully!');
43
46
  }
44
47
  catch (error) {
45
48
  this.error(error.message);
46
49
  }
47
50
  }
48
- async injectIdIntoFile(filePath, content, id) {
51
+ async injectIdIntoMeta(filePath, id) {
49
52
  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}`);
53
+ const metaPath = filePath.replace(/\.[^.]+$/, '.json');
54
+ let meta = {};
55
+ try {
56
+ const existing = await fs.readFile(metaPath, 'utf8');
57
+ meta = JSON.parse(existing);
56
58
  }
57
- else {
58
- return;
59
+ catch (error) {
60
+ if (error.code !== 'ENOENT')
61
+ throw error;
59
62
  }
60
- await fs.writeFile(filePath, updated);
63
+ meta.id = id;
64
+ await fs.writeFile(metaPath, JSON.stringify(meta, null, 2) + '\n');
61
65
  }
62
66
  async loadSnippets(path) {
63
67
  const fs = await import('node:fs/promises');
64
68
  const snippets = [];
69
+ const SNIPPET_EXTENSIONS = new Set(['.css', '.html', '.js', '.php', '.txt']);
65
70
  try {
66
71
  const files = await fs.readdir(path);
67
72
  for (const file of files) {
68
- if (file.endsWith('.php')) {
69
- const filePath = `${path}/${file}`;
70
- const content = await fs.readFile(filePath, 'utf8');
71
- const meta = this.parseMetaFromContent(content);
72
- snippets.push({
73
- code: content,
74
- id: meta.id,
75
- name: meta.name ?? file.replace('.php', ''),
76
- path: filePath,
77
- });
73
+ const ext = file.slice(file.lastIndexOf('.'));
74
+ if (!SNIPPET_EXTENSIONS.has(ext))
75
+ continue;
76
+ const filePath = `${path}/${file}`;
77
+ const metaPath = filePath.slice(0, filePath.lastIndexOf('.')) + '.json';
78
+ const content = await fs.readFile(filePath, 'utf8');
79
+ let id;
80
+ let name;
81
+ try {
82
+ const metaContent = await fs.readFile(metaPath, 'utf8');
83
+ const meta = JSON.parse(metaContent);
84
+ id = meta.id ? Number(meta.id) : undefined;
85
+ name = meta.name ? String(meta.name) : undefined;
86
+ }
87
+ catch (error) {
88
+ if (error.code !== 'ENOENT')
89
+ throw error;
78
90
  }
91
+ snippets.push({
92
+ code: content,
93
+ id,
94
+ name: name ?? file.slice(0, file.lastIndexOf('.')),
95
+ path: filePath,
96
+ });
79
97
  }
80
98
  }
81
99
  catch (error) {
@@ -83,15 +101,8 @@ export default class Push extends LoopressCommand {
83
101
  }
84
102
  return snippets;
85
103
  }
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
- }
94
- async pushSnippet(snippet, url, headers, dryRun, adapter) {
104
+ async pushSnippet(snippet, ctx) {
105
+ const { adapter, dryRun, headers, url } = ctx;
95
106
  if (dryRun) {
96
107
  this.log(`📝 [DRY RUN] Would push snippet: ${snippet.name}`);
97
108
  this.log(`📄 Code preview: ${snippet.code.slice(0, 100)}...`);
@@ -109,7 +120,7 @@ export default class Push extends LoopressCommand {
109
120
  this.log(`➕ Creating new snippet: ${snippet.name}`);
110
121
  const response = await got.post(endpoint, { headers, json: payload }).json();
111
122
  const created = adapter.fromRemote(response);
112
- await this.injectIdIntoFile(snippet.path, snippet.code, created.id);
123
+ await this.injectIdIntoMeta(snippet.path, created.id);
113
124
  this.log(`✅ Created: ${snippet.name} (id: ${created.id})`);
114
125
  }
115
126
  catch (error) {
@@ -30,10 +30,10 @@ export class AuthManager {
30
30
  }
31
31
  }
32
32
  getAuthFilePath() {
33
- return join(this.homeDir, '.lps', 'auth.json');
33
+ return join(this.homeDir, '.loopress', 'auth.json');
34
34
  }
35
35
  setAuth(auth) {
36
- const dir = join(this.homeDir, '.lps');
36
+ const dir = join(this.homeDir, '.loopress');
37
37
  if (!existsSync(dir))
38
38
  mkdirSync(dir, { recursive: true });
39
39
  const filePath = this.getAuthFilePath();
@@ -14,13 +14,13 @@ export class ProjectConfigManager {
14
14
  return ProjectConfigManager.instance;
15
15
  }
16
16
  ensureConfigDir() {
17
- const dir = join(this.homeDir, '.lps');
17
+ const dir = join(this.homeDir, '.loopress');
18
18
  if (!existsSync(dir)) {
19
19
  mkdirSync(dir, { recursive: true });
20
20
  }
21
21
  }
22
22
  getConfigFilePath() {
23
- return join(this.homeDir, '.lps', 'config.json');
23
+ return join(this.homeDir, '.loopress', 'config.json');
24
24
  }
25
25
  getCurrentEnv() {
26
26
  const project = this.getCurrentProject();
@@ -9,6 +9,6 @@ export declare abstract class LoopressCommand extends Command {
9
9
  protected siteConfig: EnvironmentConfig;
10
10
  buildAuthHeaders(): Promise<Record<string, string>>;
11
11
  init(): Promise<void>;
12
+ protected resolveSnippetPlugin(flag?: string): Promise<'code-snippets' | 'wpcode'>;
12
13
  protected resolveSnippetsPath(override?: string): Promise<string>;
13
- protected resolveStylesPath(override?: string): Promise<string>;
14
14
  }
package/dist/lib/base.js CHANGED
@@ -27,6 +27,18 @@ export class LoopressCommand extends Command {
27
27
  }
28
28
  async init() {
29
29
  await super.init();
30
+ const localConfig = await readLocalConfig();
31
+ if (localConfig.projectId) {
32
+ const project = configManager.getProject(localConfig.projectId);
33
+ if (!project) {
34
+ this.error(`Project "${localConfig.projectId}" (from loopress.json) not found. Run \`lps project config\` to configure it.`);
35
+ }
36
+ if (!project.currentEnv || !project.environments[project.currentEnv]) {
37
+ this.error(`Project "${localConfig.projectId}" has no active environment. Run \`lps project config\` to configure one.`);
38
+ }
39
+ this.siteConfig = project.environments[project.currentEnv];
40
+ return;
41
+ }
30
42
  const env = configManager.getCurrentEnv();
31
43
  if (env) {
32
44
  this.siteConfig = env;
@@ -34,16 +46,16 @@ export class LoopressCommand extends Command {
34
46
  }
35
47
  this.error('No environment configured. Run `lps project config` first.');
36
48
  }
37
- async resolveSnippetsPath(override) {
38
- if (override)
39
- return override;
49
+ async resolveSnippetPlugin(flag) {
50
+ if (flag)
51
+ return flag;
40
52
  const config = await readLocalConfig();
41
- return join(config.rootDir ?? '.', config.snippets ?? 'snippets');
53
+ return config.snippetPlugin ?? 'wpcode';
42
54
  }
43
- async resolveStylesPath(override) {
55
+ async resolveSnippetsPath(override) {
44
56
  if (override)
45
57
  return override;
46
58
  const config = await readLocalConfig();
47
- return join(config.rootDir ?? '.', config.styles ?? 'styles');
59
+ return join(config.rootDir ?? '.', config.snippetsDir ?? 'snippets');
48
60
  }
49
61
  }
@@ -0,0 +1,7 @@
1
+ import { LoopressCommand } from './base.js';
2
+ export declare abstract class PushCommand extends LoopressCommand {
3
+ protected dryRun: boolean;
4
+ catch(err: Error): Promise<void>;
5
+ protected recordDeployment(status: 'failure' | 'success'): Promise<void>;
6
+ protected recordSuccess(): Promise<void>;
7
+ }
@@ -0,0 +1,32 @@
1
+ import got from 'got';
2
+ import { authManager } from '../config/auth.manager.js';
3
+ import { LoopressCommand } from './base.js';
4
+ const API_URL = process.env.LPS_API_URL ?? 'https://api.loopress.dev';
5
+ export class PushCommand extends LoopressCommand {
6
+ dryRun = false;
7
+ async catch(err) {
8
+ if (!this.dryRun && this.siteConfig) {
9
+ await this.recordDeployment('failure');
10
+ }
11
+ return super.catch(err);
12
+ }
13
+ async recordDeployment(status) {
14
+ const token = process.env.LPS_TOKEN ?? authManager.getAuth()?.token ?? null;
15
+ if (!token)
16
+ return;
17
+ try {
18
+ await got.post(`${API_URL}/deployments`, {
19
+ headers: { Authorization: `Bearer ${token}` },
20
+ json: { status, url: this.siteConfig.url },
21
+ timeout: { request: 3000 },
22
+ });
23
+ }
24
+ catch {
25
+ // non-blocking: recording must never interrupt the push flow
26
+ }
27
+ }
28
+ async recordSuccess() {
29
+ if (!this.dryRun)
30
+ await this.recordDeployment('success');
31
+ }
32
+ }
@@ -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,9 @@
1
1
  export interface LoopressLocalConfig {
2
+ plugins?: Record<string, string>;
3
+ projectId?: string;
2
4
  rootDir?: string;
3
- snippets?: string;
4
- styles?: string;
5
+ snippetPlugin?: 'code-snippets' | 'wpcode';
6
+ snippetsDir?: string;
5
7
  }
6
8
  export declare function readLocalConfig(): Promise<LoopressLocalConfig>;
9
+ 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
+ }
@@ -31,7 +31,7 @@ class CodeSnippetsPlugin {
31
31
  }
32
32
  toPayload(name, code, path) {
33
33
  return {
34
- code,
34
+ code: code.replace(/^<\?php\s*/i, ''),
35
35
  desc: `Imported from ${path}`,
36
36
  name,
37
37
  tags: ['cli-import'],