@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.
- package/README.md +88 -334
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +73 -0
- package/dist/commands/{styles → plugin}/pull.d.ts +1 -1
- package/dist/commands/plugin/pull.js +48 -0
- package/dist/commands/{styles → plugin}/push.d.ts +4 -4
- package/dist/commands/plugin/push.js +113 -0
- package/dist/commands/plugin/require.d.ts +17 -0
- package/dist/commands/plugin/require.js +74 -0
- package/dist/commands/project/config.js +2 -2
- package/dist/commands/{snippets → snippet}/list.d.ts +1 -1
- package/dist/commands/{snippets → snippet}/list.js +3 -3
- package/dist/commands/{snippets → snippet}/pull.d.ts +4 -1
- package/dist/commands/{snippets → snippet}/pull.js +26 -37
- package/dist/commands/{snippets → snippet}/push.d.ts +4 -5
- package/dist/commands/{snippets → snippet}/push.js +50 -39
- package/dist/config/auth.manager.js +2 -2
- package/dist/config/project-config.manager.js +2 -2
- package/dist/lib/base.d.ts +1 -1
- package/dist/lib/base.js +18 -6
- package/dist/lib/push-command.d.ts +7 -0
- package/dist/lib/push-command.js +32 -0
- package/dist/types/plugin.d.ts +15 -0
- package/dist/types/plugin.js +1 -0
- package/dist/utils/loopress-config.d.ts +5 -2
- package/dist/utils/loopress-config.js +8 -3
- package/dist/utils/plugins.d.ts +27 -0
- package/dist/utils/plugins.js +34 -0
- package/dist/utils/snippet-plugin.js +1 -1
- package/oclif.manifest.json +217 -131
- package/package.json +14 -19
- package/dist/commands/styles/pull.js +0 -52
- 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 {
|
|
3
|
+
import { PushCommand } from '../../lib/push-command.js';
|
|
4
4
|
import { getSnippetPlugin } from '../../utils/snippet-plugin.js';
|
|
5
|
-
export default class Push extends
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
40
|
+
const adapter = getSnippetPlugin(resolvedPlugin);
|
|
39
41
|
for (const snippet of snippets) {
|
|
40
|
-
await this.pushSnippet(snippet,
|
|
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
|
|
51
|
+
async injectIdIntoMeta(filePath, id) {
|
|
49
52
|
const fs = await import('node:fs/promises');
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code !== 'ENOENT')
|
|
61
|
+
throw error;
|
|
59
62
|
}
|
|
60
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
87
|
-
const
|
|
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.
|
|
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, '.
|
|
33
|
+
return join(this.homeDir, '.loopress', 'auth.json');
|
|
34
34
|
}
|
|
35
35
|
setAuth(auth) {
|
|
36
|
-
const dir = join(this.homeDir, '.
|
|
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, '.
|
|
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, '.
|
|
23
|
+
return join(this.homeDir, '.loopress', 'config.json');
|
|
24
24
|
}
|
|
25
25
|
getCurrentEnv() {
|
|
26
26
|
const project = this.getCurrentProject();
|
package/dist/lib/base.d.ts
CHANGED
|
@@ -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
|
|
38
|
-
if (
|
|
39
|
-
return
|
|
49
|
+
async resolveSnippetPlugin(flag) {
|
|
50
|
+
if (flag)
|
|
51
|
+
return flag;
|
|
40
52
|
const config = await readLocalConfig();
|
|
41
|
-
return
|
|
53
|
+
return config.snippetPlugin ?? 'wpcode';
|
|
42
54
|
}
|
|
43
|
-
async
|
|
55
|
+
async resolveSnippetsPath(override) {
|
|
44
56
|
if (override)
|
|
45
57
|
return override;
|
|
46
58
|
const config = await readLocalConfig();
|
|
47
|
-
return join(config.rootDir ?? '.', config.
|
|
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
|
-
|
|
4
|
-
|
|
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.
|
|
5
|
+
const configPath = join(process.cwd(), 'loopress.json');
|
|
5
6
|
if (!existsSync(configPath))
|
|
6
7
|
return {};
|
|
7
8
|
try {
|
|
8
|
-
const
|
|
9
|
-
return
|
|
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
|
+
}
|