wave-agent-sdk 0.14.0 → 0.14.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/core/plugin.d.ts +2 -2
- package/dist/core/plugin.d.ts.map +1 -1
- package/dist/core/plugin.js +7 -7
- package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
- package/dist/managers/backgroundTaskManager.js +0 -12
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +1 -1
- package/dist/services/MarketplaceService.d.ts +53 -12
- package/dist/services/MarketplaceService.d.ts.map +1 -1
- package/dist/services/MarketplaceService.js +311 -123
- package/dist/services/configurationService.d.ts +17 -1
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +104 -0
- package/dist/services/pluginLoader.d.ts +6 -0
- package/dist/services/pluginLoader.d.ts.map +1 -1
- package/dist/services/pluginLoader.js +52 -7
- package/dist/types/configuration.d.ts +7 -0
- package/dist/types/configuration.d.ts.map +1 -1
- package/dist/types/marketplace.d.ts +28 -1
- package/dist/types/marketplace.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +13 -1
- package/dist/types/plugins.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/plugin.ts +13 -7
- package/src/managers/backgroundTaskManager.ts +1 -20
- package/src/managers/pluginManager.ts +4 -1
- package/src/services/MarketplaceService.ts +425 -134
- package/src/services/configurationService.ts +131 -0
- package/src/services/pluginLoader.ts +66 -7
- package/src/types/configuration.ts +8 -0
- package/src/types/marketplace.ts +26 -1
- package/src/types/plugins.ts +13 -1
|
@@ -8,14 +8,21 @@ import {
|
|
|
8
8
|
InstalledPlugin,
|
|
9
9
|
InstalledPluginsRegistry,
|
|
10
10
|
MarketplaceManifest,
|
|
11
|
+
MarketplacePluginEntry,
|
|
12
|
+
MarketplaceSource,
|
|
11
13
|
} from "../types/marketplace.js";
|
|
12
14
|
import { GitService } from "./GitService.js";
|
|
15
|
+
import { ConfigurationService } from "./configurationService.js";
|
|
16
|
+
import type { MarketplaceConfig, Scope } from "../types/configuration.js";
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Marketplace Service
|
|
16
20
|
*
|
|
17
21
|
* Handles local plugin marketplace registration, plugin installation,
|
|
18
22
|
* and state management for installed plugins.
|
|
23
|
+
*
|
|
24
|
+
* Marketplace declarations are now scoped (user/project/local) via settings files.
|
|
25
|
+
* known_marketplaces.json is kept as a cache for installLocation/lastUpdated metadata.
|
|
19
26
|
*/
|
|
20
27
|
export class MarketplaceService {
|
|
21
28
|
private static isLockedInProcess = false;
|
|
@@ -27,6 +34,8 @@ export class MarketplaceService {
|
|
|
27
34
|
private cacheDir: string;
|
|
28
35
|
private marketplacesDir: string;
|
|
29
36
|
private gitService: GitService;
|
|
37
|
+
private configurationService: ConfigurationService;
|
|
38
|
+
private workdir: string;
|
|
30
39
|
private static readonly BUILTIN_MARKETPLACE: KnownMarketplace = {
|
|
31
40
|
name: "wave-plugins-official",
|
|
32
41
|
source: {
|
|
@@ -36,7 +45,12 @@ export class MarketplaceService {
|
|
|
36
45
|
autoUpdate: true,
|
|
37
46
|
};
|
|
38
47
|
|
|
39
|
-
constructor(
|
|
48
|
+
constructor(
|
|
49
|
+
workdir: string = process.cwd(),
|
|
50
|
+
configurationService: ConfigurationService = new ConfigurationService(),
|
|
51
|
+
) {
|
|
52
|
+
this.workdir = workdir;
|
|
53
|
+
this.configurationService = configurationService;
|
|
40
54
|
this.pluginsDir = getPluginsDir();
|
|
41
55
|
this.knownMarketplacesPath = path.join(
|
|
42
56
|
this.pluginsDir,
|
|
@@ -53,6 +67,7 @@ export class MarketplaceService {
|
|
|
53
67
|
this.gitService = new GitService();
|
|
54
68
|
|
|
55
69
|
this.ensureDirectoryStructure();
|
|
70
|
+
this.runMigration();
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
/**
|
|
@@ -68,6 +83,44 @@ export class MarketplaceService {
|
|
|
68
83
|
);
|
|
69
84
|
}
|
|
70
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Backwards compatibility migration: migrate entries from known_marketplaces.json
|
|
88
|
+
* to user-level settings if they aren't already declared there.
|
|
89
|
+
*/
|
|
90
|
+
private async runMigration(): Promise<void> {
|
|
91
|
+
try {
|
|
92
|
+
const cacheRegistry = await this.getCacheRegistry();
|
|
93
|
+
const scopedMarketplaces =
|
|
94
|
+
this.configurationService.getMergedMarketplaces(this.workdir);
|
|
95
|
+
const userMarketplaces = this.configurationService.getScopedMarketplaces(
|
|
96
|
+
this.workdir,
|
|
97
|
+
"user",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const entriesToMigrate = (cacheRegistry?.marketplaces ?? []).filter(
|
|
101
|
+
(m) => !scopedMarketplaces[m.name] && !userMarketplaces[m.name],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (entriesToMigrate.length > 0) {
|
|
105
|
+
for (const m of entriesToMigrate) {
|
|
106
|
+
if (m.name === MarketplaceService.BUILTIN_MARKETPLACE.name) continue;
|
|
107
|
+
const config: MarketplaceConfig = {
|
|
108
|
+
source: m.source,
|
|
109
|
+
autoUpdate: m.autoUpdate,
|
|
110
|
+
};
|
|
111
|
+
await this.configurationService.addMarketplaceToScope(
|
|
112
|
+
this.workdir,
|
|
113
|
+
"user",
|
|
114
|
+
m.name,
|
|
115
|
+
config,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// Migration failure should not block startup
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
71
124
|
/**
|
|
72
125
|
* Check if a lock file is stale by reading its PID and checking if the process is alive.
|
|
73
126
|
* Returns true if the lock is stale and safe to remove.
|
|
@@ -77,15 +130,14 @@ export class MarketplaceService {
|
|
|
77
130
|
const content = await fs.readFile(this.lockPath, "utf-8");
|
|
78
131
|
const pid = parseInt(content.trim(), 10);
|
|
79
132
|
if (isNaN(pid)) return true;
|
|
80
|
-
// Check if the process is still running
|
|
81
133
|
try {
|
|
82
134
|
process.kill(pid, 0);
|
|
83
|
-
return false;
|
|
135
|
+
return false;
|
|
84
136
|
} catch {
|
|
85
|
-
return true;
|
|
137
|
+
return true;
|
|
86
138
|
}
|
|
87
139
|
} catch {
|
|
88
|
-
return true;
|
|
140
|
+
return true;
|
|
89
141
|
}
|
|
90
142
|
}
|
|
91
143
|
|
|
@@ -99,7 +151,7 @@ export class MarketplaceService {
|
|
|
99
151
|
}
|
|
100
152
|
|
|
101
153
|
let lockFd: Awaited<ReturnType<typeof fs.open>> | undefined;
|
|
102
|
-
const maxRetries = 600;
|
|
154
|
+
const maxRetries = 600;
|
|
103
155
|
const retryDelay = 100;
|
|
104
156
|
|
|
105
157
|
for (let i = 0; i < maxRetries; i++) {
|
|
@@ -113,7 +165,6 @@ export class MarketplaceService {
|
|
|
113
165
|
"code" in error &&
|
|
114
166
|
error.code === "EEXIST"
|
|
115
167
|
) {
|
|
116
|
-
// Check for stale lock every 60 retries (every ~6 seconds)
|
|
117
168
|
if (i > 0 && i % 60 === 0) {
|
|
118
169
|
const stale = await this.isStaleLock();
|
|
119
170
|
if (stale) {
|
|
@@ -134,7 +185,6 @@ export class MarketplaceService {
|
|
|
134
185
|
);
|
|
135
186
|
}
|
|
136
187
|
|
|
137
|
-
// Write PID into the lock file for stale lock detection
|
|
138
188
|
await fs.writeFile(this.lockPath, String(process.pid), "utf-8");
|
|
139
189
|
|
|
140
190
|
MarketplaceService.isLockedInProcess = true;
|
|
@@ -148,36 +198,96 @@ export class MarketplaceService {
|
|
|
148
198
|
}
|
|
149
199
|
|
|
150
200
|
/**
|
|
151
|
-
* Loads the
|
|
201
|
+
* Loads the cache registry from known_marketplaces.json
|
|
202
|
+
* Returns null if the file doesn't exist or has no parseable content (for first-run detection).
|
|
152
203
|
*/
|
|
153
|
-
async
|
|
204
|
+
async getCacheRegistry(): Promise<KnownMarketplacesRegistry | null> {
|
|
154
205
|
if (!existsSync(this.knownMarketplacesPath)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const content = await fs.readFile(this.knownMarketplacesPath, "utf-8");
|
|
210
|
+
if (!content.trim()) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return JSON.parse(content);
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Legacy method: loads known marketplaces with builtin injection.
|
|
221
|
+
* @deprecated Use listMarketplaces() instead, which combines scoped settings.
|
|
222
|
+
*/
|
|
223
|
+
async getKnownMarketplaces(): Promise<KnownMarketplacesRegistry> {
|
|
224
|
+
const cache = await this.getCacheRegistry();
|
|
225
|
+
// If cache is null (file doesn't exist or has no parseable content), inject builtin
|
|
226
|
+
if (cache === null) {
|
|
155
227
|
return {
|
|
156
228
|
marketplaces: [
|
|
157
229
|
{
|
|
158
230
|
...MarketplaceService.BUILTIN_MARKETPLACE,
|
|
159
231
|
isBuiltin: true,
|
|
232
|
+
declaredScope: "builtin",
|
|
160
233
|
},
|
|
161
234
|
],
|
|
162
235
|
};
|
|
163
236
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
237
|
+
// File has valid JSON - respect user's explicit choice even if empty
|
|
238
|
+
const hasBuiltin = cache.marketplaces.some(
|
|
239
|
+
(m) => m.name === MarketplaceService.BUILTIN_MARKETPLACE.name,
|
|
240
|
+
);
|
|
241
|
+
return {
|
|
242
|
+
marketplaces: cache.marketplaces.map((m) => ({
|
|
243
|
+
...m,
|
|
244
|
+
isBuiltin:
|
|
245
|
+
m.isBuiltin || m.name === MarketplaceService.BUILTIN_MARKETPLACE.name,
|
|
246
|
+
declaredScope: m.declaredScope ?? (hasBuiltin ? "builtin" : "user"),
|
|
247
|
+
})),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Updates the cache registry with metadata for a marketplace
|
|
253
|
+
*/
|
|
254
|
+
private async updateCacheMarketplace(
|
|
255
|
+
name: string,
|
|
256
|
+
metadata: Partial<KnownMarketplace>,
|
|
257
|
+
): Promise<void> {
|
|
258
|
+
const registry = await this.getCacheRegistry();
|
|
259
|
+
const marketplaces = registry?.marketplaces ?? [];
|
|
260
|
+
const existingIndex = marketplaces.findIndex((m) => m.name === name);
|
|
261
|
+
if (existingIndex >= 0) {
|
|
262
|
+
marketplaces[existingIndex] = {
|
|
263
|
+
...marketplaces[existingIndex],
|
|
264
|
+
...metadata,
|
|
265
|
+
};
|
|
266
|
+
} else {
|
|
267
|
+
marketplaces.push({
|
|
268
|
+
name,
|
|
269
|
+
source: metadata.source || { source: "directory", path: "" },
|
|
270
|
+
...metadata,
|
|
271
|
+
});
|
|
180
272
|
}
|
|
273
|
+
const tmpPath = `${this.knownMarketplacesPath}.tmp`;
|
|
274
|
+
await fs.writeFile(tmpPath, JSON.stringify({ marketplaces }, null, 2));
|
|
275
|
+
await fs.rename(tmpPath, this.knownMarketplacesPath);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Removes a marketplace from the cache registry
|
|
280
|
+
*/
|
|
281
|
+
private async removeFromCache(name: string): Promise<void> {
|
|
282
|
+
const registry = await this.getCacheRegistry();
|
|
283
|
+
const marketplaces = registry?.marketplaces ?? [];
|
|
284
|
+
const filtered = marketplaces.filter((m) => m.name !== name);
|
|
285
|
+
const tmpPath = `${this.knownMarketplacesPath}.tmp`;
|
|
286
|
+
await fs.writeFile(
|
|
287
|
+
tmpPath,
|
|
288
|
+
JSON.stringify({ marketplaces: filtered }, null, 2),
|
|
289
|
+
);
|
|
290
|
+
await fs.rename(tmpPath, this.knownMarketplacesPath);
|
|
181
291
|
}
|
|
182
292
|
|
|
183
293
|
/**
|
|
@@ -199,17 +309,6 @@ export class MarketplaceService {
|
|
|
199
309
|
}
|
|
200
310
|
}
|
|
201
311
|
|
|
202
|
-
/**
|
|
203
|
-
* Saves the known marketplaces registry
|
|
204
|
-
*/
|
|
205
|
-
async saveKnownMarketplaces(
|
|
206
|
-
registry: KnownMarketplacesRegistry,
|
|
207
|
-
): Promise<void> {
|
|
208
|
-
const tmpPath = `${this.knownMarketplacesPath}.tmp`;
|
|
209
|
-
await fs.writeFile(tmpPath, JSON.stringify(registry, null, 2));
|
|
210
|
-
await fs.rename(tmpPath, this.knownMarketplacesPath);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
312
|
/**
|
|
214
313
|
* Saves the installed plugins registry
|
|
215
314
|
*/
|
|
@@ -221,24 +320,46 @@ export class MarketplaceService {
|
|
|
221
320
|
await fs.rename(tmpPath, this.installedPluginsPath);
|
|
222
321
|
}
|
|
223
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Finds the first existing marketplace manifest path.
|
|
325
|
+
* Prefers .wave-plugin/ for backward compatibility, falls back to .claude-plugin/.
|
|
326
|
+
* Returns null if neither exists.
|
|
327
|
+
*/
|
|
328
|
+
private async findMarketplaceManifestPath(
|
|
329
|
+
dir: string,
|
|
330
|
+
): Promise<string | null> {
|
|
331
|
+
const waveManifestPath = path.join(dir, ".wave-plugin", "marketplace.json");
|
|
332
|
+
const claudeManifestPath = path.join(
|
|
333
|
+
dir,
|
|
334
|
+
".claude-plugin",
|
|
335
|
+
"marketplace.json",
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (existsSync(waveManifestPath)) {
|
|
339
|
+
return waveManifestPath;
|
|
340
|
+
}
|
|
341
|
+
if (existsSync(claudeManifestPath)) {
|
|
342
|
+
return claudeManifestPath;
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
224
347
|
/**
|
|
225
348
|
* Loads a marketplace manifest from a local path
|
|
226
349
|
*/
|
|
227
350
|
async loadMarketplaceManifest(
|
|
228
351
|
marketplacePath: string,
|
|
229
352
|
): Promise<MarketplaceManifest> {
|
|
230
|
-
const manifestPath =
|
|
231
|
-
marketplacePath
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
throw new Error(`Marketplace manifest not found at ${manifestPath}`);
|
|
353
|
+
const manifestPath =
|
|
354
|
+
await this.findMarketplaceManifestPath(marketplacePath);
|
|
355
|
+
if (!manifestPath) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`Marketplace manifest not found at ${marketplacePath}. Neither .wave-plugin/marketplace.json nor .claude-plugin/marketplace.json exists.`,
|
|
358
|
+
);
|
|
237
359
|
}
|
|
238
360
|
const content = await fs.readFile(manifestPath, "utf-8");
|
|
239
361
|
const manifest = JSON.parse(content);
|
|
240
362
|
|
|
241
|
-
// Basic validation
|
|
242
363
|
if (
|
|
243
364
|
!manifest.name ||
|
|
244
365
|
!manifest.plugins ||
|
|
@@ -253,25 +374,60 @@ export class MarketplaceService {
|
|
|
253
374
|
/**
|
|
254
375
|
* Resolves the local path for a marketplace
|
|
255
376
|
*/
|
|
256
|
-
public getMarketplacePath(
|
|
257
|
-
if (
|
|
258
|
-
return
|
|
259
|
-
} else if (
|
|
260
|
-
return path.join(this.marketplacesDir,
|
|
377
|
+
public getMarketplacePath(source: MarketplaceSource): string {
|
|
378
|
+
if (source.source === "directory") {
|
|
379
|
+
return source.path;
|
|
380
|
+
} else if (source.source === "github") {
|
|
381
|
+
return path.join(this.marketplacesDir, source.repo);
|
|
261
382
|
} else {
|
|
262
|
-
|
|
263
|
-
const hash = crypto
|
|
264
|
-
.createHash("md5")
|
|
265
|
-
.update(marketplace.source.url)
|
|
266
|
-
.digest("hex");
|
|
383
|
+
const hash = crypto.createHash("md5").update(source.url).digest("hex");
|
|
267
384
|
return path.join(this.marketplacesDir, hash);
|
|
268
385
|
}
|
|
269
386
|
}
|
|
270
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Builds a KnownMarketplace from a scoped config, enriched with cache metadata
|
|
390
|
+
*/
|
|
391
|
+
private async buildMarketplaceEntry(
|
|
392
|
+
name: string,
|
|
393
|
+
config: MarketplaceConfig,
|
|
394
|
+
cache: KnownMarketplace | undefined,
|
|
395
|
+
declaredScope: "user" | "project" | "local" | "builtin",
|
|
396
|
+
): Promise<KnownMarketplace> {
|
|
397
|
+
return {
|
|
398
|
+
name,
|
|
399
|
+
source: config.source,
|
|
400
|
+
autoUpdate: config.autoUpdate ?? cache?.autoUpdate,
|
|
401
|
+
lastUpdated: cache?.lastUpdated,
|
|
402
|
+
isBuiltin: name === MarketplaceService.BUILTIN_MARKETPLACE.name,
|
|
403
|
+
declaredScope,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Finds which scope declared a marketplace (user, project, local, or builtin)
|
|
409
|
+
*/
|
|
410
|
+
getMarketplaceDeclaringSource(name: string): Scope | "builtin" | null {
|
|
411
|
+
if (name === MarketplaceService.BUILTIN_MARKETPLACE.name) return "builtin";
|
|
412
|
+
|
|
413
|
+
const scopes: Scope[] = ["local", "project", "user"];
|
|
414
|
+
for (const scope of scopes) {
|
|
415
|
+
const scoped = this.configurationService.getScopedMarketplaces(
|
|
416
|
+
this.workdir,
|
|
417
|
+
scope,
|
|
418
|
+
);
|
|
419
|
+
if (scoped[name]) return scope;
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
271
424
|
/**
|
|
272
425
|
* Adds a new marketplace (local directory, GitHub repo, or Git URL)
|
|
273
426
|
*/
|
|
274
|
-
async addMarketplace(
|
|
427
|
+
async addMarketplace(
|
|
428
|
+
input: string,
|
|
429
|
+
scope: Scope = "user",
|
|
430
|
+
): Promise<KnownMarketplace> {
|
|
275
431
|
return this.withLock(async () => {
|
|
276
432
|
let marketplace: KnownMarketplace;
|
|
277
433
|
|
|
@@ -287,7 +443,6 @@ export class MarketplaceService {
|
|
|
287
443
|
!path.isAbsolute(input) &&
|
|
288
444
|
!input.startsWith("."))
|
|
289
445
|
) {
|
|
290
|
-
// Git or GitHub repo
|
|
291
446
|
let urlOrRepo = input;
|
|
292
447
|
let ref: string | undefined;
|
|
293
448
|
|
|
@@ -295,11 +450,11 @@ export class MarketplaceService {
|
|
|
295
450
|
[urlOrRepo, ref] = input.split("#");
|
|
296
451
|
}
|
|
297
452
|
|
|
298
|
-
const
|
|
299
|
-
? {
|
|
300
|
-
: {
|
|
453
|
+
const tempSource: MarketplaceSource = isFullUrl
|
|
454
|
+
? { source: "git", url: urlOrRepo, ref }
|
|
455
|
+
: { source: "github", repo: urlOrRepo, ref };
|
|
301
456
|
|
|
302
|
-
const targetPath = this.getMarketplacePath(
|
|
457
|
+
const targetPath = this.getMarketplacePath(tempSource);
|
|
303
458
|
|
|
304
459
|
if (!existsSync(targetPath)) {
|
|
305
460
|
try {
|
|
@@ -329,7 +484,6 @@ export class MarketplaceService {
|
|
|
329
484
|
lastUpdated: new Date().toISOString(),
|
|
330
485
|
};
|
|
331
486
|
} else {
|
|
332
|
-
// Local directory format
|
|
333
487
|
const absolutePath = path.resolve(input);
|
|
334
488
|
let manifest: MarketplaceManifest;
|
|
335
489
|
try {
|
|
@@ -348,54 +502,97 @@ export class MarketplaceService {
|
|
|
348
502
|
};
|
|
349
503
|
}
|
|
350
504
|
|
|
351
|
-
const
|
|
505
|
+
const config: MarketplaceConfig = {
|
|
506
|
+
source: marketplace.source,
|
|
507
|
+
autoUpdate: marketplace.autoUpdate,
|
|
508
|
+
};
|
|
352
509
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
510
|
+
await this.configurationService.addMarketplaceToScope(
|
|
511
|
+
this.workdir,
|
|
512
|
+
scope,
|
|
513
|
+
marketplace.name,
|
|
514
|
+
config,
|
|
356
515
|
);
|
|
357
|
-
if (existingIndex >= 0) {
|
|
358
|
-
registry.marketplaces[existingIndex] = marketplace;
|
|
359
|
-
} else {
|
|
360
|
-
registry.marketplaces.push(marketplace);
|
|
361
|
-
}
|
|
362
516
|
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
517
|
+
// Update cache with metadata
|
|
518
|
+
await this.updateCacheMarketplace(marketplace.name, {
|
|
519
|
+
source: marketplace.source,
|
|
520
|
+
autoUpdate: marketplace.autoUpdate,
|
|
521
|
+
lastUpdated: marketplace.lastUpdated,
|
|
522
|
+
});
|
|
366
523
|
|
|
367
|
-
await this.saveKnownMarketplaces(registry);
|
|
368
524
|
return marketplace;
|
|
369
525
|
});
|
|
370
526
|
}
|
|
371
527
|
|
|
372
528
|
/**
|
|
373
|
-
* Lists all registered marketplaces
|
|
529
|
+
* Lists all registered marketplaces by combining scoped settings + built-in
|
|
374
530
|
*/
|
|
375
531
|
async listMarketplaces(): Promise<KnownMarketplace[]> {
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
532
|
+
const scopedMarketplaces = this.configurationService.getMergedMarketplaces(
|
|
533
|
+
this.workdir,
|
|
534
|
+
);
|
|
535
|
+
const cacheRegistry = await this.getCacheRegistry();
|
|
536
|
+
const cacheMap = new Map(
|
|
537
|
+
(cacheRegistry?.marketplaces ?? []).map((m) => [m.name, m]),
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const result: KnownMarketplace[] = [];
|
|
541
|
+
|
|
542
|
+
// Add built-in marketplace
|
|
543
|
+
result.push({
|
|
544
|
+
...MarketplaceService.BUILTIN_MARKETPLACE,
|
|
545
|
+
isBuiltin: true,
|
|
546
|
+
declaredScope: "builtin",
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Add all scoped marketplaces (local overrides project overrides user)
|
|
550
|
+
for (const [name, config] of Object.entries(scopedMarketplaces)) {
|
|
551
|
+
if (name === MarketplaceService.BUILTIN_MARKETPLACE.name) continue;
|
|
552
|
+
const cache = cacheMap.get(name);
|
|
553
|
+
const declaredScope = this.getMarketplaceDeclaringSource(name) as
|
|
554
|
+
| "user"
|
|
555
|
+
| "project"
|
|
556
|
+
| "local";
|
|
557
|
+
result.push(
|
|
558
|
+
await this.buildMarketplaceEntry(name, config, cache, declaredScope),
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Add cache entries not yet in scoped settings (backwards compatibility)
|
|
563
|
+
for (const [name, cache] of cacheMap.entries()) {
|
|
564
|
+
if (name === MarketplaceService.BUILTIN_MARKETPLACE.name) continue;
|
|
565
|
+
if (scopedMarketplaces[name]) continue;
|
|
566
|
+
result.push({
|
|
567
|
+
...cache,
|
|
568
|
+
isBuiltin: false,
|
|
569
|
+
declaredScope: cache.declaredScope ?? "user",
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return result;
|
|
381
574
|
}
|
|
382
575
|
|
|
383
576
|
/**
|
|
384
|
-
* Removes a marketplace by name
|
|
577
|
+
* Removes a marketplace by name from the specified scope
|
|
385
578
|
*/
|
|
386
|
-
async removeMarketplace(name: string): Promise<void> {
|
|
579
|
+
async removeMarketplace(name: string, scope?: Scope): Promise<void> {
|
|
387
580
|
return this.withLock(async () => {
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
registry.marketplaces = registry.marketplaces.filter(
|
|
391
|
-
(m) => m.name !== name,
|
|
392
|
-
);
|
|
581
|
+
const targetScope =
|
|
582
|
+
scope || this.getMarketplaceDeclaringSource(name) || "user";
|
|
393
583
|
|
|
394
|
-
if (
|
|
395
|
-
throw new Error(
|
|
584
|
+
if (targetScope === "builtin") {
|
|
585
|
+
throw new Error("Cannot remove built-in marketplace");
|
|
396
586
|
}
|
|
397
587
|
|
|
398
|
-
await this.
|
|
588
|
+
await this.configurationService.removeMarketplaceFromScope(
|
|
589
|
+
this.workdir,
|
|
590
|
+
targetScope,
|
|
591
|
+
name,
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
// Also remove from cache
|
|
595
|
+
await this.removeFromCache(name);
|
|
399
596
|
});
|
|
400
597
|
}
|
|
401
598
|
|
|
@@ -407,10 +604,10 @@ export class MarketplaceService {
|
|
|
407
604
|
options?: { updatePlugins?: boolean },
|
|
408
605
|
): Promise<void> {
|
|
409
606
|
return this.withLock(async () => {
|
|
410
|
-
const
|
|
607
|
+
const marketplaces = await this.listMarketplaces();
|
|
411
608
|
const toUpdate = name
|
|
412
|
-
?
|
|
413
|
-
:
|
|
609
|
+
? marketplaces.filter((m) => m.name === name)
|
|
610
|
+
: marketplaces;
|
|
414
611
|
|
|
415
612
|
if (name && toUpdate.length === 0) {
|
|
416
613
|
throw new Error(`Marketplace ${name} not found`);
|
|
@@ -430,7 +627,7 @@ export class MarketplaceService {
|
|
|
430
627
|
);
|
|
431
628
|
continue;
|
|
432
629
|
}
|
|
433
|
-
const targetPath = this.getMarketplacePath(marketplace);
|
|
630
|
+
const targetPath = this.getMarketplacePath(marketplace.source);
|
|
434
631
|
if (existsSync(targetPath)) {
|
|
435
632
|
await this.gitService.pull(targetPath);
|
|
436
633
|
} else {
|
|
@@ -447,13 +644,17 @@ export class MarketplaceService {
|
|
|
447
644
|
);
|
|
448
645
|
}
|
|
449
646
|
}
|
|
450
|
-
// For directory source, we just re-validate the manifest
|
|
451
647
|
const manifest = await this.loadMarketplaceManifest(
|
|
452
|
-
this.getMarketplacePath(marketplace),
|
|
648
|
+
this.getMarketplacePath(marketplace.source),
|
|
453
649
|
);
|
|
454
650
|
|
|
455
651
|
marketplace.lastUpdated = new Date().toISOString();
|
|
456
652
|
|
|
653
|
+
// Update cache metadata
|
|
654
|
+
await this.updateCacheMarketplace(marketplace.name, {
|
|
655
|
+
lastUpdated: marketplace.lastUpdated,
|
|
656
|
+
});
|
|
657
|
+
|
|
457
658
|
if (options?.updatePlugins) {
|
|
458
659
|
const installedRegistry = await this.getInstalledPlugins();
|
|
459
660
|
const pluginsToUpdate = installedRegistry.plugins.filter(
|
|
@@ -505,8 +706,6 @@ export class MarketplaceService {
|
|
|
505
706
|
`Some marketplaces failed to update:\n${errors.join("\n")}`,
|
|
506
707
|
);
|
|
507
708
|
}
|
|
508
|
-
|
|
509
|
-
await this.saveKnownMarketplaces(registry);
|
|
510
709
|
});
|
|
511
710
|
}
|
|
512
711
|
|
|
@@ -515,17 +714,20 @@ export class MarketplaceService {
|
|
|
515
714
|
*/
|
|
516
715
|
async autoUpdateAll(): Promise<void> {
|
|
517
716
|
return this.withLock(async () => {
|
|
518
|
-
const
|
|
519
|
-
|
|
717
|
+
const scopedMarketplaces =
|
|
718
|
+
this.configurationService.getMergedMarketplaces(this.workdir);
|
|
719
|
+
const toAutoUpdate = Object.entries(scopedMarketplaces)
|
|
720
|
+
.filter(([, config]) => config.autoUpdate)
|
|
721
|
+
.map(([name]) => name);
|
|
520
722
|
|
|
521
|
-
for (const
|
|
723
|
+
for (const marketplaceName of toAutoUpdate) {
|
|
522
724
|
try {
|
|
523
|
-
await this.updateMarketplace(
|
|
725
|
+
await this.updateMarketplace(marketplaceName, {
|
|
524
726
|
updatePlugins: true,
|
|
525
727
|
});
|
|
526
728
|
} catch (error) {
|
|
527
729
|
console.error(
|
|
528
|
-
`Auto-update failed for marketplace "${
|
|
730
|
+
`Auto-update failed for marketplace "${marketplaceName}":`,
|
|
529
731
|
error,
|
|
530
732
|
);
|
|
531
733
|
}
|
|
@@ -538,16 +740,101 @@ export class MarketplaceService {
|
|
|
538
740
|
*/
|
|
539
741
|
async toggleAutoUpdate(name: string, enabled: boolean): Promise<void> {
|
|
540
742
|
return this.withLock(async () => {
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
if (!marketplace) {
|
|
743
|
+
const declaringSource = this.getMarketplaceDeclaringSource(name);
|
|
744
|
+
if (!declaringSource || declaringSource === "builtin") {
|
|
544
745
|
throw new Error(`Marketplace ${name} not found`);
|
|
545
746
|
}
|
|
546
|
-
|
|
547
|
-
|
|
747
|
+
|
|
748
|
+
const scoped = this.configurationService.getScopedMarketplaces(
|
|
749
|
+
this.workdir,
|
|
750
|
+
declaringSource,
|
|
751
|
+
);
|
|
752
|
+
const config = scoped[name];
|
|
753
|
+
if (!config) {
|
|
754
|
+
throw new Error(`Marketplace ${name} not found`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
config.autoUpdate = enabled;
|
|
758
|
+
await this.configurationService.addMarketplaceToScope(
|
|
759
|
+
this.workdir,
|
|
760
|
+
declaringSource,
|
|
761
|
+
name,
|
|
762
|
+
config,
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
// Also update cache
|
|
766
|
+
await this.updateCacheMarketplace(name, { autoUpdate: enabled });
|
|
548
767
|
});
|
|
549
768
|
}
|
|
550
769
|
|
|
770
|
+
/**
|
|
771
|
+
* Resolves a plugin source into a consistent format for installation.
|
|
772
|
+
* Handles both string sources (local paths or git URLs) and object-style MarketplaceSource.
|
|
773
|
+
*/
|
|
774
|
+
private resolvePluginSource(
|
|
775
|
+
pluginEntry: MarketplacePluginEntry,
|
|
776
|
+
marketplacePath: string,
|
|
777
|
+
): { isGit: boolean; url?: string; ref?: string; localPath: string } {
|
|
778
|
+
const { source } = pluginEntry;
|
|
779
|
+
|
|
780
|
+
if (typeof source === "string") {
|
|
781
|
+
// String source: could be a git URL or a relative local path
|
|
782
|
+
const isGitUrl =
|
|
783
|
+
source.startsWith("http://") ||
|
|
784
|
+
source.startsWith("https://") ||
|
|
785
|
+
source.startsWith("git@") ||
|
|
786
|
+
source.startsWith("ssh://");
|
|
787
|
+
|
|
788
|
+
if (isGitUrl) {
|
|
789
|
+
let url = source;
|
|
790
|
+
let ref: string | undefined;
|
|
791
|
+
if (url.includes("#")) {
|
|
792
|
+
[url, ref] = url.split("#");
|
|
793
|
+
}
|
|
794
|
+
return { isGit: true, url, ref, localPath: "" };
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Relative local path
|
|
798
|
+
return { isGit: false, localPath: path.resolve(marketplacePath, source) };
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Object-style source
|
|
802
|
+
if (source.source === "git") {
|
|
803
|
+
return { isGit: true, url: source.url, ref: source.ref, localPath: "" };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (source.source === "github") {
|
|
807
|
+
return {
|
|
808
|
+
isGit: true,
|
|
809
|
+
url: `https://github.com/${source.repo}.git`,
|
|
810
|
+
ref: source.ref,
|
|
811
|
+
localPath: "",
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (source.source === "url") {
|
|
816
|
+
let url = source.url;
|
|
817
|
+
let ref = source.ref;
|
|
818
|
+
if (url.includes("#")) {
|
|
819
|
+
[url, ref] = url.split("#");
|
|
820
|
+
}
|
|
821
|
+
return { isGit: true, url, ref, localPath: "" };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (source.source === "directory") {
|
|
825
|
+
return {
|
|
826
|
+
isGit: false,
|
|
827
|
+
localPath: path.resolve(marketplacePath, source.path),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Exhaustiveness: this should be unreachable given the union type
|
|
832
|
+
const _exhaustive: never = source;
|
|
833
|
+
throw new Error(
|
|
834
|
+
`Unsupported plugin source type: ${(_exhaustive as { source: string }).source}`,
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
|
|
551
838
|
/**
|
|
552
839
|
* Installs a plugin from a marketplace
|
|
553
840
|
*/
|
|
@@ -567,7 +854,7 @@ export class MarketplaceService {
|
|
|
567
854
|
throw new Error(`Marketplace ${marketplaceName} not found`);
|
|
568
855
|
}
|
|
569
856
|
|
|
570
|
-
const marketplacePath = this.getMarketplacePath(marketplace);
|
|
857
|
+
const marketplacePath = this.getMarketplacePath(marketplace.source);
|
|
571
858
|
const manifest = await this.loadMarketplaceManifest(marketplacePath);
|
|
572
859
|
const pluginEntry = manifest.plugins.find((p) => p.name === pluginName);
|
|
573
860
|
if (!pluginEntry) {
|
|
@@ -576,36 +863,46 @@ export class MarketplaceService {
|
|
|
576
863
|
);
|
|
577
864
|
}
|
|
578
865
|
|
|
579
|
-
const
|
|
580
|
-
pluginEntry.source.startsWith("http://") ||
|
|
581
|
-
pluginEntry.source.startsWith("https://") ||
|
|
582
|
-
pluginEntry.source.startsWith("git@") ||
|
|
583
|
-
pluginEntry.source.startsWith("ssh://");
|
|
866
|
+
const resolved = this.resolvePluginSource(pluginEntry, marketplacePath);
|
|
584
867
|
|
|
585
868
|
let pluginSrcPath: string;
|
|
586
869
|
let tempCloneDir: string | undefined;
|
|
587
870
|
|
|
588
871
|
try {
|
|
589
|
-
if (
|
|
872
|
+
if (resolved.isGit) {
|
|
590
873
|
tempCloneDir = path.join(this.tmpDir, `clone-${Date.now()}`);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
await this.gitService.clone(url, tempCloneDir, ref);
|
|
874
|
+
await this.gitService.clone(
|
|
875
|
+
resolved.url!,
|
|
876
|
+
tempCloneDir,
|
|
877
|
+
resolved.ref,
|
|
878
|
+
);
|
|
597
879
|
pluginSrcPath = tempCloneDir;
|
|
598
880
|
} else {
|
|
599
|
-
pluginSrcPath =
|
|
881
|
+
pluginSrcPath = resolved.localPath;
|
|
600
882
|
}
|
|
601
883
|
|
|
602
|
-
|
|
884
|
+
let pluginManifestPath: string | undefined;
|
|
885
|
+
const wavePluginPath = path.join(
|
|
603
886
|
pluginSrcPath,
|
|
604
887
|
".wave-plugin",
|
|
605
888
|
"plugin.json",
|
|
606
889
|
);
|
|
607
|
-
|
|
608
|
-
|
|
890
|
+
const claudePluginPath = path.join(
|
|
891
|
+
pluginSrcPath,
|
|
892
|
+
".claude-plugin",
|
|
893
|
+
"plugin.json",
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
if (existsSync(wavePluginPath)) {
|
|
897
|
+
pluginManifestPath = wavePluginPath;
|
|
898
|
+
} else if (existsSync(claudePluginPath)) {
|
|
899
|
+
pluginManifestPath = claudePluginPath;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (!pluginManifestPath) {
|
|
903
|
+
throw new Error(
|
|
904
|
+
`Plugin manifest not found at ${pluginSrcPath}. Neither .wave-plugin/plugin.json nor .claude-plugin/plugin.json exists.`,
|
|
905
|
+
);
|
|
609
906
|
}
|
|
610
907
|
|
|
611
908
|
const pluginManifestContent = await fs.readFile(
|
|
@@ -615,15 +912,14 @@ export class MarketplaceService {
|
|
|
615
912
|
const pluginManifest = JSON.parse(pluginManifestContent);
|
|
616
913
|
const version = pluginManifest.version || "1.0.0";
|
|
617
914
|
|
|
618
|
-
// Atomic installation
|
|
619
915
|
const tmpPluginDir = path.join(
|
|
620
916
|
this.tmpDir,
|
|
621
917
|
`${pluginName}-${Date.now()}`,
|
|
622
918
|
);
|
|
623
919
|
try {
|
|
624
|
-
if (
|
|
920
|
+
if (resolved.isGit) {
|
|
625
921
|
await fs.rename(pluginSrcPath, tmpPluginDir);
|
|
626
|
-
tempCloneDir = undefined;
|
|
922
|
+
tempCloneDir = undefined;
|
|
627
923
|
} else {
|
|
628
924
|
await fs.cp(pluginSrcPath, tmpPluginDir, { recursive: true });
|
|
629
925
|
}
|
|
@@ -665,14 +961,12 @@ export class MarketplaceService {
|
|
|
665
961
|
await this.saveInstalledPlugins(installedRegistry);
|
|
666
962
|
return installedPlugin;
|
|
667
963
|
} catch (error) {
|
|
668
|
-
// Cleanup tmp dir if it exists
|
|
669
964
|
if (existsSync(tmpPluginDir)) {
|
|
670
965
|
await fs.rm(tmpPluginDir, { recursive: true, force: true });
|
|
671
966
|
}
|
|
672
967
|
throw error;
|
|
673
968
|
}
|
|
674
969
|
} catch (error) {
|
|
675
|
-
// Cleanup temp clone dir if it exists
|
|
676
970
|
if (tempCloneDir && existsSync(tempCloneDir)) {
|
|
677
971
|
await fs.rm(tempCloneDir, { recursive: true, force: true });
|
|
678
972
|
}
|
|
@@ -712,16 +1006,13 @@ export class MarketplaceService {
|
|
|
712
1006
|
|
|
713
1007
|
const pluginToRemove = installedRegistry.plugins[pluginIndex];
|
|
714
1008
|
|
|
715
|
-
// Remove from registry first
|
|
716
1009
|
installedRegistry.plugins.splice(pluginIndex, 1);
|
|
717
1010
|
await this.saveInstalledPlugins(installedRegistry);
|
|
718
1011
|
|
|
719
|
-
// Check if any other project is still using this same cache path
|
|
720
1012
|
const isStillReferenced = installedRegistry.plugins.some(
|
|
721
1013
|
(p) => p.cachePath === pluginToRemove.cachePath,
|
|
722
1014
|
);
|
|
723
1015
|
|
|
724
|
-
// Only remove cached files if no other references exist
|
|
725
1016
|
if (!isStillReferenced && existsSync(pluginToRemove.cachePath)) {
|
|
726
1017
|
await fs.rm(pluginToRemove.cachePath, { recursive: true, force: true });
|
|
727
1018
|
}
|