@kzheart_/mc-pilot 0.2.1 → 0.3.1
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/dist/commands/plugin.d.ts +2 -0
- package/dist/commands/plugin.js +93 -0
- package/dist/index.js +2 -0
- package/dist/instance/PluginCatalogManager.d.ts +18 -0
- package/dist/instance/PluginCatalogManager.js +151 -0
- package/dist/instance/ServerInstanceManager.d.ts +2 -0
- package/dist/instance/ServerInstanceManager.js +30 -5
- package/dist/util/instance-types.d.ts +1 -0
- package/dist/util/paths.d.ts +2 -0
- package/dist/util/paths.js +6 -0
- package/dist/util/plugin-types.d.ts +14 -0
- package/dist/util/plugin-types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { PluginCatalogManager } from "../instance/PluginCatalogManager.js";
|
|
3
|
+
import { MctError } from "../util/errors.js";
|
|
4
|
+
import { wrapCommand } from "../util/command.js";
|
|
5
|
+
function parseCommaSeparated(value) {
|
|
6
|
+
if (!value)
|
|
7
|
+
return undefined;
|
|
8
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
export function createPluginCommand() {
|
|
11
|
+
const plugin = new Command("plugin")
|
|
12
|
+
.description("Manage the plugin center catalog");
|
|
13
|
+
plugin.addCommand(new Command("list")
|
|
14
|
+
.description("List plugins in the catalog")
|
|
15
|
+
.option("--query <query>", "Search by name, id, or description")
|
|
16
|
+
.action(wrapCommand(async (_context, { options }) => {
|
|
17
|
+
const manager = new PluginCatalogManager();
|
|
18
|
+
const plugins = await manager.list(options.query);
|
|
19
|
+
return { plugins, count: plugins.length };
|
|
20
|
+
})));
|
|
21
|
+
plugin.addCommand(new Command("info")
|
|
22
|
+
.description("Show plugin details")
|
|
23
|
+
.argument("<id>", "Plugin ID")
|
|
24
|
+
.action(wrapCommand(async (_context, { args }) => {
|
|
25
|
+
const manager = new PluginCatalogManager();
|
|
26
|
+
return manager.get(args[0]);
|
|
27
|
+
})));
|
|
28
|
+
plugin.addCommand(new Command("add")
|
|
29
|
+
.description("Add a plugin JAR to the catalog (id/name auto-derived from filename)")
|
|
30
|
+
.argument("<jar-path>", "Path to the plugin JAR file")
|
|
31
|
+
.option("--id <id>", "Override plugin ID")
|
|
32
|
+
.option("--name <name>", "Override display name")
|
|
33
|
+
.action(wrapCommand(async (_context, { args, options }) => {
|
|
34
|
+
const manager = new PluginCatalogManager();
|
|
35
|
+
return manager.add(args[0], {
|
|
36
|
+
id: options.id,
|
|
37
|
+
name: options.name
|
|
38
|
+
});
|
|
39
|
+
})));
|
|
40
|
+
plugin.addCommand(new Command("update")
|
|
41
|
+
.description("Update plugin metadata")
|
|
42
|
+
.argument("<id>", "Plugin ID")
|
|
43
|
+
.option("--name <name>", "Display name")
|
|
44
|
+
.option("--version <version>", "Version")
|
|
45
|
+
.option("--description <description>", "Description")
|
|
46
|
+
.option("--author <author>", "Author")
|
|
47
|
+
.option("--dependencies <deps>", "Dependency plugin IDs (comma-separated)")
|
|
48
|
+
.option("--tags <tags>", "Tags (comma-separated)")
|
|
49
|
+
.action(wrapCommand(async (_context, { args, options }) => {
|
|
50
|
+
const manager = new PluginCatalogManager();
|
|
51
|
+
return manager.update(args[0], {
|
|
52
|
+
name: options.name,
|
|
53
|
+
version: options.version,
|
|
54
|
+
description: options.description,
|
|
55
|
+
author: options.author,
|
|
56
|
+
dependencies: parseCommaSeparated(options.dependencies),
|
|
57
|
+
tags: parseCommaSeparated(options.tags)
|
|
58
|
+
});
|
|
59
|
+
})));
|
|
60
|
+
plugin.addCommand(new Command("remove")
|
|
61
|
+
.description("Remove a plugin from the catalog")
|
|
62
|
+
.argument("<id>", "Plugin ID")
|
|
63
|
+
.action(wrapCommand(async (_context, { args }) => {
|
|
64
|
+
const manager = new PluginCatalogManager();
|
|
65
|
+
const removed = await manager.remove(args[0]);
|
|
66
|
+
return { removed: removed.id, name: removed.name };
|
|
67
|
+
})));
|
|
68
|
+
plugin.addCommand(new Command("install")
|
|
69
|
+
.description("Install a plugin (with dependencies) to a server")
|
|
70
|
+
.argument("<id>", "Plugin ID")
|
|
71
|
+
.requiredOption("--server <name>", "Target server instance name")
|
|
72
|
+
.option("--project <name>", "Project name")
|
|
73
|
+
.action(wrapCommand(async (context, { args, options }) => {
|
|
74
|
+
const project = options.project ?? context.projectName;
|
|
75
|
+
if (!project) {
|
|
76
|
+
throw new MctError({ code: "NO_PROJECT", message: "No project specified. Use --project or run from a project directory." }, 4);
|
|
77
|
+
}
|
|
78
|
+
const manager = new PluginCatalogManager();
|
|
79
|
+
return manager.install(args[0], project, options.server);
|
|
80
|
+
})));
|
|
81
|
+
plugin.addCommand(new Command("resolve")
|
|
82
|
+
.description("Resolve dependency tree for plugins")
|
|
83
|
+
.argument("<ids...>", "Plugin IDs to resolve")
|
|
84
|
+
.action(wrapCommand(async (_context, { args }) => {
|
|
85
|
+
const manager = new PluginCatalogManager();
|
|
86
|
+
const resolved = await manager.resolve(args);
|
|
87
|
+
return {
|
|
88
|
+
order: resolved.map((p) => p.id),
|
|
89
|
+
plugins: resolved
|
|
90
|
+
};
|
|
91
|
+
})));
|
|
92
|
+
return plugin;
|
|
93
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { createServerCommand } from "./commands/server.js";
|
|
|
24
24
|
import { createSignCommand } from "./commands/sign.js";
|
|
25
25
|
import { createStatusCommand } from "./commands/status.js";
|
|
26
26
|
import { createWaitCommand } from "./commands/wait.js";
|
|
27
|
+
import { createPluginCommand } from "./commands/plugin.js";
|
|
27
28
|
import { createInitCommand, createDeployCommand, createUpCommand, createDownCommand, createUseCommand } from "./commands/project.js";
|
|
28
29
|
import { attachGlobalOptions, wrapCommand } from "./util/command.js";
|
|
29
30
|
export function buildProgram() {
|
|
@@ -64,6 +65,7 @@ export function buildProgram() {
|
|
|
64
65
|
// Instance management
|
|
65
66
|
program.addCommand(createServerCommand());
|
|
66
67
|
program.addCommand(createClientCommand());
|
|
68
|
+
program.addCommand(createPluginCommand());
|
|
67
69
|
// Game interaction commands
|
|
68
70
|
program.addCommand(createChatCommand());
|
|
69
71
|
program.addCommand(createInputCommand());
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PluginCatalog, PluginEntry } from "../util/plugin-types.js";
|
|
2
|
+
export declare class PluginCatalogManager {
|
|
3
|
+
private pluginsDir;
|
|
4
|
+
private jarsDir;
|
|
5
|
+
constructor();
|
|
6
|
+
loadCatalog(): Promise<PluginCatalog>;
|
|
7
|
+
private saveCatalog;
|
|
8
|
+
list(query?: string): Promise<PluginEntry[]>;
|
|
9
|
+
get(id: string): Promise<PluginEntry>;
|
|
10
|
+
add(jarPath: string, overrides?: Partial<Omit<PluginEntry, "jarFile" | "addedAt">>): Promise<PluginEntry>;
|
|
11
|
+
update(id: string, fields: Partial<Omit<PluginEntry, "id" | "jarFile" | "addedAt">>): Promise<PluginEntry>;
|
|
12
|
+
remove(id: string): Promise<PluginEntry>;
|
|
13
|
+
resolve(ids: string[]): Promise<PluginEntry[]>;
|
|
14
|
+
install(id: string, project: string, serverName: string): Promise<{
|
|
15
|
+
installed: string[];
|
|
16
|
+
serverPluginsDir: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolvePluginsDir, resolvePluginJarsDir, resolveServerInstanceDir } from "../util/paths.js";
|
|
4
|
+
import { MctError } from "../util/errors.js";
|
|
5
|
+
const CATALOG_FILE = "catalog.json";
|
|
6
|
+
export class PluginCatalogManager {
|
|
7
|
+
pluginsDir;
|
|
8
|
+
jarsDir;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.pluginsDir = resolvePluginsDir();
|
|
11
|
+
this.jarsDir = resolvePluginJarsDir();
|
|
12
|
+
}
|
|
13
|
+
async loadCatalog() {
|
|
14
|
+
try {
|
|
15
|
+
const content = await readFile(path.join(this.pluginsDir, CATALOG_FILE), "utf-8");
|
|
16
|
+
return JSON.parse(content);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { plugins: [] };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async saveCatalog(catalog) {
|
|
23
|
+
await mkdir(this.pluginsDir, { recursive: true });
|
|
24
|
+
await writeFile(path.join(this.pluginsDir, CATALOG_FILE), JSON.stringify(catalog, null, 2), "utf-8");
|
|
25
|
+
}
|
|
26
|
+
async list(query) {
|
|
27
|
+
const catalog = await this.loadCatalog();
|
|
28
|
+
if (!query)
|
|
29
|
+
return catalog.plugins;
|
|
30
|
+
const q = query.toLowerCase();
|
|
31
|
+
return catalog.plugins.filter((p) => p.id.toLowerCase().includes(q) ||
|
|
32
|
+
p.name.toLowerCase().includes(q) ||
|
|
33
|
+
p.description.toLowerCase().includes(q) ||
|
|
34
|
+
p.tags.some((t) => t.toLowerCase().includes(q)));
|
|
35
|
+
}
|
|
36
|
+
async get(id) {
|
|
37
|
+
const catalog = await this.loadCatalog();
|
|
38
|
+
const entry = catalog.plugins.find((p) => p.id === id);
|
|
39
|
+
if (!entry) {
|
|
40
|
+
throw new MctError({ code: "PLUGIN_NOT_FOUND", message: `Plugin '${id}' not found in catalog` }, 4);
|
|
41
|
+
}
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
async add(jarPath, overrides) {
|
|
45
|
+
const catalog = await this.loadCatalog();
|
|
46
|
+
const resolvedJar = path.resolve(jarPath);
|
|
47
|
+
const originalName = path.basename(resolvedJar, ".jar");
|
|
48
|
+
// Derive id from filename if not provided
|
|
49
|
+
const id = overrides?.id || originalName.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
50
|
+
if (catalog.plugins.some((p) => p.id === id)) {
|
|
51
|
+
throw new MctError({ code: "PLUGIN_EXISTS", message: `Plugin '${id}' already exists in catalog` }, 4);
|
|
52
|
+
}
|
|
53
|
+
// Keep original JAR filename
|
|
54
|
+
const jarFileName = path.basename(resolvedJar);
|
|
55
|
+
await mkdir(this.jarsDir, { recursive: true });
|
|
56
|
+
await copyFile(resolvedJar, path.join(this.jarsDir, jarFileName));
|
|
57
|
+
const entry = {
|
|
58
|
+
id,
|
|
59
|
+
name: overrides?.name || originalName,
|
|
60
|
+
version: overrides?.version || "",
|
|
61
|
+
description: overrides?.description || "",
|
|
62
|
+
author: overrides?.author || "",
|
|
63
|
+
jarFile: jarFileName,
|
|
64
|
+
dependencies: overrides?.dependencies || [],
|
|
65
|
+
tags: overrides?.tags || [],
|
|
66
|
+
addedAt: new Date().toISOString()
|
|
67
|
+
};
|
|
68
|
+
catalog.plugins.push(entry);
|
|
69
|
+
await this.saveCatalog(catalog);
|
|
70
|
+
return entry;
|
|
71
|
+
}
|
|
72
|
+
async update(id, fields) {
|
|
73
|
+
const catalog = await this.loadCatalog();
|
|
74
|
+
const entry = catalog.plugins.find((p) => p.id === id);
|
|
75
|
+
if (!entry) {
|
|
76
|
+
throw new MctError({ code: "PLUGIN_NOT_FOUND", message: `Plugin '${id}' not found in catalog` }, 4);
|
|
77
|
+
}
|
|
78
|
+
if (fields.name !== undefined)
|
|
79
|
+
entry.name = fields.name;
|
|
80
|
+
if (fields.version !== undefined)
|
|
81
|
+
entry.version = fields.version;
|
|
82
|
+
if (fields.description !== undefined)
|
|
83
|
+
entry.description = fields.description;
|
|
84
|
+
if (fields.author !== undefined)
|
|
85
|
+
entry.author = fields.author;
|
|
86
|
+
if (fields.dependencies !== undefined)
|
|
87
|
+
entry.dependencies = fields.dependencies;
|
|
88
|
+
if (fields.tags !== undefined)
|
|
89
|
+
entry.tags = fields.tags;
|
|
90
|
+
await this.saveCatalog(catalog);
|
|
91
|
+
return entry;
|
|
92
|
+
}
|
|
93
|
+
async remove(id) {
|
|
94
|
+
const catalog = await this.loadCatalog();
|
|
95
|
+
const index = catalog.plugins.findIndex((p) => p.id === id);
|
|
96
|
+
if (index === -1) {
|
|
97
|
+
throw new MctError({ code: "PLUGIN_NOT_FOUND", message: `Plugin '${id}' not found in catalog` }, 4);
|
|
98
|
+
}
|
|
99
|
+
const [entry] = catalog.plugins.splice(index, 1);
|
|
100
|
+
try {
|
|
101
|
+
await rm(path.join(this.jarsDir, entry.jarFile));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// JAR file may already be deleted
|
|
105
|
+
}
|
|
106
|
+
await this.saveCatalog(catalog);
|
|
107
|
+
return entry;
|
|
108
|
+
}
|
|
109
|
+
async resolve(ids) {
|
|
110
|
+
const catalog = await this.loadCatalog();
|
|
111
|
+
const byId = new Map(catalog.plugins.map((p) => [p.id, p]));
|
|
112
|
+
const resolved = [];
|
|
113
|
+
const visited = new Set();
|
|
114
|
+
const visiting = new Set();
|
|
115
|
+
const visit = (id) => {
|
|
116
|
+
if (visited.has(id))
|
|
117
|
+
return;
|
|
118
|
+
if (visiting.has(id)) {
|
|
119
|
+
throw new MctError({ code: "CIRCULAR_DEPENDENCY", message: `Circular dependency detected involving '${id}'` }, 4);
|
|
120
|
+
}
|
|
121
|
+
const entry = byId.get(id);
|
|
122
|
+
if (!entry) {
|
|
123
|
+
throw new MctError({ code: "MISSING_DEPENDENCY", message: `Missing dependency: '${id}'` }, 4);
|
|
124
|
+
}
|
|
125
|
+
visiting.add(id);
|
|
126
|
+
for (const dep of entry.dependencies) {
|
|
127
|
+
visit(dep);
|
|
128
|
+
}
|
|
129
|
+
visiting.delete(id);
|
|
130
|
+
visited.add(id);
|
|
131
|
+
resolved.push(entry);
|
|
132
|
+
};
|
|
133
|
+
for (const id of ids) {
|
|
134
|
+
visit(id);
|
|
135
|
+
}
|
|
136
|
+
return resolved;
|
|
137
|
+
}
|
|
138
|
+
async install(id, project, serverName) {
|
|
139
|
+
const toInstall = await this.resolve([id]);
|
|
140
|
+
const serverPluginsDir = path.join(resolveServerInstanceDir(project, serverName), "plugins");
|
|
141
|
+
await mkdir(serverPluginsDir, { recursive: true });
|
|
142
|
+
const installed = [];
|
|
143
|
+
for (const entry of toInstall) {
|
|
144
|
+
const src = path.join(this.jarsDir, entry.jarFile);
|
|
145
|
+
const dest = path.join(serverPluginsDir, entry.jarFile);
|
|
146
|
+
await copyFile(src, dest);
|
|
147
|
+
installed.push(entry.id);
|
|
148
|
+
}
|
|
149
|
+
return { installed, serverPluginsDir };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -44,6 +44,7 @@ export declare class ServerInstanceManager {
|
|
|
44
44
|
startedAt: string;
|
|
45
45
|
logPath: string;
|
|
46
46
|
instanceDir: string;
|
|
47
|
+
stdinPipe?: string;
|
|
47
48
|
running: boolean;
|
|
48
49
|
stale: boolean;
|
|
49
50
|
} | {
|
|
@@ -54,6 +55,7 @@ export declare class ServerInstanceManager {
|
|
|
54
55
|
startedAt: string;
|
|
55
56
|
logPath: string;
|
|
56
57
|
instanceDir: string;
|
|
58
|
+
stdinPipe?: string;
|
|
57
59
|
running: boolean;
|
|
58
60
|
}>;
|
|
59
61
|
waitReady(serverName: string, timeoutSeconds: number): Promise<{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { copyFile, mkdir, readdir, readFile, writeFile, unlink } from "node:fs/promises";
|
|
2
2
|
import { mkdirSync, openSync } from "node:fs";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
3
|
+
import { spawn, execSync } from "node:child_process";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { resolveProjectDir, resolveServerInstanceDir } from "../util/paths.js";
|
|
6
6
|
import { MctError } from "../util/errors.js";
|
|
@@ -57,17 +57,34 @@ export class ServerInstanceManager {
|
|
|
57
57
|
await writeFile(path.join(instanceDir, "eula.txt"), "eula=true\n", "utf8");
|
|
58
58
|
}
|
|
59
59
|
const logsDir = path.join(this.globalState.getRootDir(), "logs");
|
|
60
|
+
const stateDir = path.join(this.globalState.getRootDir(), "state");
|
|
60
61
|
mkdirSync(logsDir, { recursive: true });
|
|
62
|
+
mkdirSync(stateDir, { recursive: true });
|
|
61
63
|
const logPath = path.join(logsDir, `server-${this.project}-${serverName}.log`);
|
|
62
64
|
const stdout = openSync(logPath, "a");
|
|
63
65
|
const jvmArgs = options.jvmArgs ?? meta.jvmArgs;
|
|
64
|
-
|
|
66
|
+
// Create a named pipe (FIFO) for stdin so external tools (GUI) can send commands
|
|
67
|
+
const stdinPipe = path.join(stateDir, `stdin-${this.project}-${serverName}.fifo`);
|
|
68
|
+
try {
|
|
69
|
+
await unlink(stdinPipe);
|
|
70
|
+
}
|
|
71
|
+
catch { /* ignore */ }
|
|
72
|
+
execSync(`mkfifo "${stdinPipe}"`);
|
|
73
|
+
// Use bash wrapper: hold FIFO write end open (fd 3) to prevent EOF,
|
|
74
|
+
// then exec java with stdin reading from the FIFO
|
|
75
|
+
const child = spawn("bash", [
|
|
76
|
+
"-c",
|
|
77
|
+
'exec 3>"$MCT_STDIN_PIPE"; exec java "$@" <"$MCT_STDIN_PIPE"',
|
|
78
|
+
"mct-server",
|
|
79
|
+
...jvmArgs, "-jar", jarFile, "nogui"
|
|
80
|
+
], {
|
|
65
81
|
cwd: instanceDir,
|
|
66
82
|
detached: true,
|
|
67
83
|
stdio: ["ignore", stdout, stdout],
|
|
68
84
|
env: {
|
|
69
85
|
...process.env,
|
|
70
|
-
MCT_SERVER_PORT: String(meta.port)
|
|
86
|
+
MCT_SERVER_PORT: String(meta.port),
|
|
87
|
+
MCT_STDIN_PIPE: stdinPipe
|
|
71
88
|
}
|
|
72
89
|
});
|
|
73
90
|
child.unref();
|
|
@@ -78,7 +95,8 @@ export class ServerInstanceManager {
|
|
|
78
95
|
port: meta.port,
|
|
79
96
|
startedAt: new Date().toISOString(),
|
|
80
97
|
logPath,
|
|
81
|
-
instanceDir
|
|
98
|
+
instanceDir,
|
|
99
|
+
stdinPipe
|
|
82
100
|
};
|
|
83
101
|
state.servers[stateKey] = entry;
|
|
84
102
|
await this.globalState.writeServerState(state);
|
|
@@ -94,6 +112,13 @@ export class ServerInstanceManager {
|
|
|
94
112
|
if (isProcessRunning(entry.pid)) {
|
|
95
113
|
killProcessTree(entry.pid);
|
|
96
114
|
}
|
|
115
|
+
// Clean up FIFO
|
|
116
|
+
if (entry.stdinPipe) {
|
|
117
|
+
try {
|
|
118
|
+
await unlink(entry.stdinPipe);
|
|
119
|
+
}
|
|
120
|
+
catch { /* ignore */ }
|
|
121
|
+
}
|
|
97
122
|
delete state.servers[stateKey];
|
|
98
123
|
await this.globalState.writeServerState(state);
|
|
99
124
|
return { running: false, stopped: true, pid: entry.pid };
|
package/dist/util/paths.d.ts
CHANGED
|
@@ -5,3 +5,5 @@ export declare function resolveProjectsDir(): string;
|
|
|
5
5
|
export declare function resolveProjectDir(project: string): string;
|
|
6
6
|
export declare function resolveServerInstanceDir(project: string, server: string): string;
|
|
7
7
|
export declare function resolveGlobalStateDir(): string;
|
|
8
|
+
export declare function resolvePluginsDir(): string;
|
|
9
|
+
export declare function resolvePluginJarsDir(): string;
|
package/dist/util/paths.js
CHANGED
|
@@ -21,3 +21,9 @@ export function resolveServerInstanceDir(project, server) {
|
|
|
21
21
|
export function resolveGlobalStateDir() {
|
|
22
22
|
return path.join(resolveMctHome(), "state");
|
|
23
23
|
}
|
|
24
|
+
export function resolvePluginsDir() {
|
|
25
|
+
return path.join(resolveMctHome(), "plugins");
|
|
26
|
+
}
|
|
27
|
+
export function resolvePluginJarsDir() {
|
|
28
|
+
return path.join(resolvePluginsDir(), "jars");
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface PluginEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
description: string;
|
|
6
|
+
author: string;
|
|
7
|
+
jarFile: string;
|
|
8
|
+
dependencies: string[];
|
|
9
|
+
tags: string[];
|
|
10
|
+
addedAt: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PluginCatalog {
|
|
13
|
+
plugins: PluginEntry[];
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|