opencode-marketplace 0.3.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.
- package/package.json +1 -1
- package/src/cli.ts +32 -2
- package/src/commands/import.ts +69 -0
- package/src/commands/install.ts +52 -20
- package/src/import-config.ts +70 -0
- package/src/paths.ts +8 -5
- package/src/registry.ts +13 -7
- package/src/types.ts +7 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cac } from "cac";
|
|
2
2
|
import { version } from "../package.json";
|
|
3
|
+
import { importPlugins } from "./commands/import";
|
|
3
4
|
import { install } from "./commands/install";
|
|
4
5
|
import { list } from "./commands/list";
|
|
5
6
|
import { scan } from "./commands/scan";
|
|
@@ -12,14 +13,36 @@ export function run(argv = process.argv) {
|
|
|
12
13
|
cli
|
|
13
14
|
.command("install <path>", "Install a plugin from a local directory or GitHub URL")
|
|
14
15
|
.option("--scope <scope>", "Installation scope (user/project)", { default: "user" })
|
|
16
|
+
.option(
|
|
17
|
+
"--target-dir <dir>",
|
|
18
|
+
"Custom installation directory (overrides default scope directory)",
|
|
19
|
+
)
|
|
15
20
|
.option("--force", "Overwrite existing components", { default: false })
|
|
16
21
|
.option("-i, --interactive", "Interactively select components to install", { default: false })
|
|
17
|
-
.action((path, options) => {
|
|
22
|
+
.action(async (path, options) => {
|
|
18
23
|
if (options.scope !== "user" && options.scope !== "project") {
|
|
19
24
|
console.error(`Invalid scope: ${options.scope}. Must be 'user' or 'project'.`);
|
|
20
25
|
process.exit(1);
|
|
21
26
|
}
|
|
22
|
-
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await install(path, options);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error instanceof Error) {
|
|
32
|
+
console.error(`\nError: ${error.message}`);
|
|
33
|
+
} else {
|
|
34
|
+
console.error("\nUnknown error occurred during installation");
|
|
35
|
+
}
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
cli
|
|
41
|
+
.command("import [config-path]", "Install plugins from import config file")
|
|
42
|
+
.option("--target-dir <dir>", "Custom installation directory (overrides ~/.config/opencode)")
|
|
43
|
+
.option("--force", "Overwrite existing components", { default: false })
|
|
44
|
+
.action((configPath, options) => {
|
|
45
|
+
return importPlugins(configPath, options);
|
|
23
46
|
});
|
|
24
47
|
|
|
25
48
|
cli
|
|
@@ -67,6 +90,13 @@ export function run(argv = process.argv) {
|
|
|
67
90
|
cli.help();
|
|
68
91
|
cli.version(version);
|
|
69
92
|
|
|
93
|
+
// Show help when no command is provided (just "opencode-marketplace")
|
|
94
|
+
// argv.length is 2 when only executable is run (process.argv includes [node, script])
|
|
95
|
+
if (argv.length <= 2) {
|
|
96
|
+
cli.outputHelp();
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
70
100
|
try {
|
|
71
101
|
cli.parse(argv);
|
|
72
102
|
} catch (error) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getDefaultImportConfigPath, loadImportConfig } from "../import-config";
|
|
2
|
+
import { install } from "./install";
|
|
3
|
+
|
|
4
|
+
export interface ImportOptions {
|
|
5
|
+
targetDir?: string;
|
|
6
|
+
force: boolean;
|
|
7
|
+
verbose?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function importPlugins(configPath: string | undefined, options: ImportOptions) {
|
|
11
|
+
const { targetDir, force, verbose } = options;
|
|
12
|
+
const actualConfigPath = configPath || getDefaultImportConfigPath();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
console.log(`Importing plugins from ${actualConfigPath}...\n`);
|
|
16
|
+
|
|
17
|
+
const config = await loadImportConfig(actualConfigPath);
|
|
18
|
+
|
|
19
|
+
if (config.plugins.length === 0) {
|
|
20
|
+
console.log("No plugins found in configuration.");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const results = {
|
|
25
|
+
installed: 0,
|
|
26
|
+
updated: 0,
|
|
27
|
+
skipped: 0,
|
|
28
|
+
failed: 0,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < config.plugins.length; i++) {
|
|
32
|
+
const source = config.plugins[i];
|
|
33
|
+
const displayNum = `[${i + 1}/${config.plugins.length}]`;
|
|
34
|
+
|
|
35
|
+
console.log(`${displayNum} ${source}`);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = await install(source, {
|
|
39
|
+
scope: "user",
|
|
40
|
+
force,
|
|
41
|
+
verbose,
|
|
42
|
+
skipIfSameHash: true,
|
|
43
|
+
targetDir,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (result.status === "installed") results.installed++;
|
|
47
|
+
else if (result.status === "updated") results.updated++;
|
|
48
|
+
else if (result.status === "skipped") results.skipped++;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
results.failed++;
|
|
51
|
+
console.error(` Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
52
|
+
}
|
|
53
|
+
console.log(""); // Empty line between plugins
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log("Import complete:");
|
|
57
|
+
console.log(` Installed: ${results.installed}`);
|
|
58
|
+
console.log(` Updated: ${results.updated}`);
|
|
59
|
+
console.log(` Skipped: ${results.skipped}`);
|
|
60
|
+
console.log(` Failed: ${results.failed}`);
|
|
61
|
+
|
|
62
|
+
if (results.failed > 0) {
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/commands/install.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface InstallOptions {
|
|
|
21
21
|
force: boolean;
|
|
22
22
|
verbose?: boolean;
|
|
23
23
|
interactive?: boolean;
|
|
24
|
+
skipIfSameHash?: boolean;
|
|
25
|
+
targetDir?: string;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
interface ConflictInfo {
|
|
@@ -29,10 +31,16 @@ interface ConflictInfo {
|
|
|
29
31
|
conflictingPlugin: string | null; // null = untracked file
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
+
export interface InstallResult {
|
|
35
|
+
status: "installed" | "updated" | "skipped";
|
|
36
|
+
pluginName: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function install(path: string, options: InstallOptions): Promise<InstallResult> {
|
|
40
|
+
const { scope, force, verbose, interactive, skipIfSameHash, targetDir } = options;
|
|
34
41
|
|
|
35
42
|
let tempDir: string | null = null;
|
|
43
|
+
|
|
36
44
|
let pluginSource: PluginSource;
|
|
37
45
|
|
|
38
46
|
try {
|
|
@@ -110,7 +118,7 @@ export async function install(path: string, options: InstallOptions) {
|
|
|
110
118
|
if (tempDir) {
|
|
111
119
|
await cleanup(tempDir);
|
|
112
120
|
}
|
|
113
|
-
return;
|
|
121
|
+
return { status: "skipped", pluginName: "" };
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
if (result.selected.length === 0) {
|
|
@@ -118,7 +126,7 @@ export async function install(path: string, options: InstallOptions) {
|
|
|
118
126
|
if (tempDir) {
|
|
119
127
|
await cleanup(tempDir);
|
|
120
128
|
}
|
|
121
|
-
return;
|
|
129
|
+
return { status: "skipped", pluginName: "" };
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
componentsToInstall = result.selected;
|
|
@@ -128,7 +136,7 @@ export async function install(path: string, options: InstallOptions) {
|
|
|
128
136
|
if (tempDir) {
|
|
129
137
|
await cleanup(tempDir);
|
|
130
138
|
}
|
|
131
|
-
return;
|
|
139
|
+
return { status: "skipped", pluginName: "" };
|
|
132
140
|
}
|
|
133
141
|
throw error;
|
|
134
142
|
}
|
|
@@ -151,14 +159,25 @@ export async function install(path: string, options: InstallOptions) {
|
|
|
151
159
|
console.log(`Installing ${pluginName} [${shortHash}]...`);
|
|
152
160
|
|
|
153
161
|
// Step 5: Check for existing installation
|
|
154
|
-
const existingPlugin = await getInstalledPlugin(pluginName, scope);
|
|
162
|
+
const existingPlugin = await getInstalledPlugin(pluginName, scope, targetDir);
|
|
163
|
+
let installStatus: "installed" | "updated" | "skipped" = "installed";
|
|
155
164
|
|
|
156
165
|
if (existingPlugin) {
|
|
157
166
|
if (existingPlugin.hash === pluginHash) {
|
|
167
|
+
if (skipIfSameHash) {
|
|
168
|
+
if (verbose) {
|
|
169
|
+
console.log(`[VERBOSE] Skipping ${pluginName} (already up to date)`);
|
|
170
|
+
}
|
|
171
|
+
if (tempDir) {
|
|
172
|
+
await cleanup(tempDir);
|
|
173
|
+
}
|
|
174
|
+
return { status: "skipped", pluginName };
|
|
175
|
+
}
|
|
158
176
|
// Same plugin, same hash - reinstall
|
|
159
177
|
if (verbose) {
|
|
160
178
|
console.log(`[VERBOSE] Reinstalling existing plugin (same hash)`);
|
|
161
179
|
}
|
|
180
|
+
installStatus = "installed";
|
|
162
181
|
} else {
|
|
163
182
|
// Same plugin, different hash - update
|
|
164
183
|
if (verbose) {
|
|
@@ -166,11 +185,12 @@ export async function install(path: string, options: InstallOptions) {
|
|
|
166
185
|
`[VERBOSE] Updating plugin from [${existingPlugin.hash.substring(0, 8)}] to [${shortHash}]`,
|
|
167
186
|
);
|
|
168
187
|
}
|
|
188
|
+
installStatus = "updated";
|
|
169
189
|
}
|
|
170
190
|
}
|
|
171
191
|
|
|
172
192
|
// Step 6: Detect conflicts
|
|
173
|
-
const conflicts = await detectConflicts(componentsToInstall, pluginName, scope);
|
|
193
|
+
const conflicts = await detectConflicts(componentsToInstall, pluginName, scope, targetDir);
|
|
174
194
|
|
|
175
195
|
if (conflicts.length > 0 && !force) {
|
|
176
196
|
console.error("\nConflict detected:");
|
|
@@ -194,7 +214,7 @@ export async function install(path: string, options: InstallOptions) {
|
|
|
194
214
|
}
|
|
195
215
|
|
|
196
216
|
// Step 7: Ensure target directories exist
|
|
197
|
-
await ensureComponentDirsExist(scope);
|
|
217
|
+
await ensureComponentDirsExist(scope, targetDir);
|
|
198
218
|
|
|
199
219
|
// Step 8: Copy components
|
|
200
220
|
const installedComponents = {
|
|
@@ -207,7 +227,13 @@ export async function install(path: string, options: InstallOptions) {
|
|
|
207
227
|
const sortedComponents = [...componentsToInstall].sort((a, b) => a.name.localeCompare(b.name));
|
|
208
228
|
|
|
209
229
|
for (const component of sortedComponents) {
|
|
210
|
-
const targetPath = getComponentTargetPath(
|
|
230
|
+
const targetPath = getComponentTargetPath(
|
|
231
|
+
pluginName,
|
|
232
|
+
component.name,
|
|
233
|
+
component.type,
|
|
234
|
+
scope,
|
|
235
|
+
targetDir,
|
|
236
|
+
);
|
|
211
237
|
|
|
212
238
|
// Remove trailing slash for copying
|
|
213
239
|
const normalizedTarget = targetPath.endsWith("/") ? targetPath.slice(0, -1) : targetPath;
|
|
@@ -233,7 +259,7 @@ export async function install(path: string, options: InstallOptions) {
|
|
|
233
259
|
}
|
|
234
260
|
|
|
235
261
|
// Step 9: Update registry
|
|
236
|
-
const registry = await loadRegistry(scope);
|
|
262
|
+
const registry = await loadRegistry(scope, targetDir);
|
|
237
263
|
|
|
238
264
|
const newPlugin: InstalledPlugin = {
|
|
239
265
|
name: pluginName,
|
|
@@ -245,7 +271,7 @@ export async function install(path: string, options: InstallOptions) {
|
|
|
245
271
|
};
|
|
246
272
|
|
|
247
273
|
registry.plugins[pluginName] = newPlugin;
|
|
248
|
-
await saveRegistry(registry, scope);
|
|
274
|
+
await saveRegistry(registry, scope, targetDir);
|
|
249
275
|
|
|
250
276
|
// Step 10: Cleanup temp directory if remote installation
|
|
251
277
|
if (tempDir) {
|
|
@@ -254,19 +280,18 @@ export async function install(path: string, options: InstallOptions) {
|
|
|
254
280
|
|
|
255
281
|
// Step 11: Print success message
|
|
256
282
|
const componentCounts = formatComponentCount(installedComponents);
|
|
257
|
-
|
|
283
|
+
const locationMsg = targetDir ? `to ${targetDir}` : `to ${scope} scope`;
|
|
284
|
+
console.log(`\nInstalled ${pluginName} (${componentCounts}) ${locationMsg}.`);
|
|
285
|
+
|
|
286
|
+
return { status: installStatus, pluginName };
|
|
258
287
|
} catch (error) {
|
|
259
288
|
// Cleanup temp directory on error
|
|
260
289
|
if (tempDir) {
|
|
261
290
|
await cleanup(tempDir);
|
|
262
291
|
}
|
|
263
292
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
} else {
|
|
267
|
-
console.error("\nUnknown error occurred during installation");
|
|
268
|
-
}
|
|
269
|
-
process.exit(1);
|
|
293
|
+
// Re-throw the error to let the caller handle it
|
|
294
|
+
throw error;
|
|
270
295
|
}
|
|
271
296
|
}
|
|
272
297
|
|
|
@@ -280,12 +305,19 @@ async function detectConflicts(
|
|
|
280
305
|
components: DiscoveredComponent[],
|
|
281
306
|
pluginName: string,
|
|
282
307
|
scope: Scope,
|
|
308
|
+
targetDir?: string,
|
|
283
309
|
): Promise<ConflictInfo[]> {
|
|
284
310
|
const conflicts: ConflictInfo[] = [];
|
|
285
|
-
const registry = await loadRegistry(scope);
|
|
311
|
+
const registry = await loadRegistry(scope, targetDir);
|
|
286
312
|
|
|
287
313
|
for (const component of components) {
|
|
288
|
-
const targetPath = getComponentTargetPath(
|
|
314
|
+
const targetPath = getComponentTargetPath(
|
|
315
|
+
pluginName,
|
|
316
|
+
component.name,
|
|
317
|
+
component.type,
|
|
318
|
+
scope,
|
|
319
|
+
targetDir,
|
|
320
|
+
);
|
|
289
321
|
|
|
290
322
|
// Remove trailing slash for existence check
|
|
291
323
|
const normalizedTarget = targetPath.endsWith("/") ? targetPath.slice(0, -1) : targetPath;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { isGitHubUrl } from "./github";
|
|
6
|
+
import type { ImportConfig } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the default import config path: ~/.config/opencode/ocm-import.json
|
|
10
|
+
*/
|
|
11
|
+
export function getDefaultImportConfigPath(): string {
|
|
12
|
+
return join(homedir(), ".config", "opencode", "ocm-import.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Loads and validates the import configuration file.
|
|
17
|
+
*
|
|
18
|
+
* @param configPath Path to the config file
|
|
19
|
+
* @returns Parsed and validated ImportConfig
|
|
20
|
+
* @throws Error if file not found, invalid JSON, or invalid schema
|
|
21
|
+
*/
|
|
22
|
+
export async function loadImportConfig(configPath: string): Promise<ImportConfig> {
|
|
23
|
+
const absolutePath = resolve(configPath);
|
|
24
|
+
|
|
25
|
+
if (!existsSync(absolutePath)) {
|
|
26
|
+
throw new Error(`Import configuration file not found: ${configPath}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
30
|
+
let config: unknown;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
config = JSON.parse(content);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Failed to parse import configuration (invalid JSON): ${error instanceof Error ? error.message : String(error)}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validation
|
|
41
|
+
if (!config || typeof config !== "object") {
|
|
42
|
+
throw new Error("Invalid import configuration: expected an object");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!("plugins" in config) || !Array.isArray(config.plugins)) {
|
|
46
|
+
throw new Error("Invalid import configuration: 'plugins' must be an array");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Resolve relative paths
|
|
50
|
+
const configDir = dirname(absolutePath);
|
|
51
|
+
const plugins: string[] = [];
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < config.plugins.length; i++) {
|
|
54
|
+
const source = config.plugins[i];
|
|
55
|
+
if (typeof source !== "string" || source.trim() === "") {
|
|
56
|
+
throw new Error(`Invalid import configuration: 'plugins[${i}]' must be a non-empty string`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const trimmedSource = source.trim();
|
|
60
|
+
|
|
61
|
+
// Resolve relative paths for local sources (not GitHub URLs and not absolute paths)
|
|
62
|
+
if (!isGitHubUrl(trimmedSource) && !isAbsolute(trimmedSource)) {
|
|
63
|
+
plugins.push(resolve(configDir, trimmedSource));
|
|
64
|
+
} else {
|
|
65
|
+
plugins.push(trimmedSource);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { plugins };
|
|
70
|
+
}
|
package/src/paths.ts
CHANGED
|
@@ -10,10 +10,10 @@ import { getComponentTargetName } from "./types";
|
|
|
10
10
|
* - User scope: "~/.config/opencode/command/"
|
|
11
11
|
* - Project scope: ".opencode/command/"
|
|
12
12
|
*/
|
|
13
|
-
export function getComponentDir(type: ComponentType, scope: Scope): string {
|
|
13
|
+
export function getComponentDir(type: ComponentType, scope: Scope, targetDir?: string): string {
|
|
14
14
|
const basePath =
|
|
15
15
|
scope === "user"
|
|
16
|
-
? join(homedir(), ".config", "opencode", type)
|
|
16
|
+
? join(targetDir || join(homedir(), ".config", "opencode"), type)
|
|
17
17
|
: join(process.cwd(), ".opencode", type);
|
|
18
18
|
|
|
19
19
|
return `${normalize(basePath)}/`;
|
|
@@ -32,8 +32,9 @@ export function getComponentTargetPath(
|
|
|
32
32
|
componentName: string,
|
|
33
33
|
type: ComponentType,
|
|
34
34
|
scope: Scope,
|
|
35
|
+
targetDir?: string,
|
|
35
36
|
): string {
|
|
36
|
-
const baseDir = getComponentDir(type, scope);
|
|
37
|
+
const baseDir = getComponentDir(type, scope, targetDir);
|
|
37
38
|
const targetName = getComponentTargetName(pluginName, componentName);
|
|
38
39
|
const fullPath = join(baseDir, targetName);
|
|
39
40
|
|
|
@@ -49,8 +50,10 @@ export function getComponentTargetPath(
|
|
|
49
50
|
* Ensures all component directories (command, agent, skill) exist for the given scope.
|
|
50
51
|
* Idempotent - safe to call multiple times.
|
|
51
52
|
*/
|
|
52
|
-
export async function ensureComponentDirsExist(scope: Scope): Promise<void> {
|
|
53
|
+
export async function ensureComponentDirsExist(scope: Scope, targetDir?: string): Promise<void> {
|
|
53
54
|
const dirs: ComponentType[] = ["command", "agent", "skill"];
|
|
54
55
|
|
|
55
|
-
await Promise.all(
|
|
56
|
+
await Promise.all(
|
|
57
|
+
dirs.map((type) => mkdir(getComponentDir(type, scope, targetDir), { recursive: true })),
|
|
58
|
+
);
|
|
56
59
|
}
|
package/src/registry.ts
CHANGED
|
@@ -7,9 +7,10 @@ import type { InstalledPlugin, PluginRegistry, Scope } from "./types";
|
|
|
7
7
|
/**
|
|
8
8
|
* Returns the path to the registry file for the given scope.
|
|
9
9
|
*/
|
|
10
|
-
export function getRegistryPath(scope: Scope): string {
|
|
10
|
+
export function getRegistryPath(scope: Scope, targetDir?: string): string {
|
|
11
11
|
if (scope === "user") {
|
|
12
|
-
|
|
12
|
+
const base = targetDir || join(homedir(), ".config", "opencode");
|
|
13
|
+
return join(base, "plugins", "installed.json");
|
|
13
14
|
}
|
|
14
15
|
// project scope
|
|
15
16
|
return join(process.cwd(), ".opencode", "plugins", "installed.json");
|
|
@@ -19,8 +20,8 @@ export function getRegistryPath(scope: Scope): string {
|
|
|
19
20
|
* Loads the plugin registry for the given scope.
|
|
20
21
|
* Returns an empty registry if the file does not exist.
|
|
21
22
|
*/
|
|
22
|
-
export async function loadRegistry(scope: Scope): Promise<PluginRegistry> {
|
|
23
|
-
const path = getRegistryPath(scope);
|
|
23
|
+
export async function loadRegistry(scope: Scope, targetDir?: string): Promise<PluginRegistry> {
|
|
24
|
+
const path = getRegistryPath(scope, targetDir);
|
|
24
25
|
|
|
25
26
|
if (!existsSync(path)) {
|
|
26
27
|
return { version: 2, plugins: {} };
|
|
@@ -47,8 +48,12 @@ export async function loadRegistry(scope: Scope): Promise<PluginRegistry> {
|
|
|
47
48
|
* Saves the plugin registry for the given scope.
|
|
48
49
|
* Uses atomic write pattern.
|
|
49
50
|
*/
|
|
50
|
-
export async function saveRegistry(
|
|
51
|
-
|
|
51
|
+
export async function saveRegistry(
|
|
52
|
+
registry: PluginRegistry,
|
|
53
|
+
scope: Scope,
|
|
54
|
+
targetDir?: string,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
const path = getRegistryPath(scope, targetDir);
|
|
52
57
|
const dir = join(path, "..");
|
|
53
58
|
|
|
54
59
|
if (!existsSync(dir)) {
|
|
@@ -66,8 +71,9 @@ export async function saveRegistry(registry: PluginRegistry, scope: Scope): Prom
|
|
|
66
71
|
export async function getInstalledPlugin(
|
|
67
72
|
name: string,
|
|
68
73
|
scope: Scope,
|
|
74
|
+
targetDir?: string,
|
|
69
75
|
): Promise<InstalledPlugin | null> {
|
|
70
|
-
const registry = await loadRegistry(scope);
|
|
76
|
+
const registry = await loadRegistry(scope, targetDir);
|
|
71
77
|
return registry.plugins[name] || null;
|
|
72
78
|
}
|
|
73
79
|
|
package/src/types.ts
CHANGED
|
@@ -41,6 +41,13 @@ export interface PluginRegistry {
|
|
|
41
41
|
plugins: Record<string, InstalledPlugin>;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Configuration for importing multiple plugins at once
|
|
46
|
+
*/
|
|
47
|
+
export interface ImportConfig {
|
|
48
|
+
plugins: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
// Validation & Helpers
|
|
45
52
|
|
|
46
53
|
/**
|