wave-agent-sdk 0.14.0 → 0.14.2

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 (88) hide show
  1. package/builtin/skills/settings/HOOKS.md +69 -0
  2. package/builtin/skills/settings/PLUGINS.md +171 -0
  3. package/builtin/skills/settings/SKILL.md +8 -3
  4. package/dist/agent.d.ts +2 -2
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +12 -3
  7. package/dist/core/plugin.d.ts +2 -2
  8. package/dist/core/plugin.d.ts.map +1 -1
  9. package/dist/core/plugin.js +7 -7
  10. package/dist/managers/aiManager.d.ts +6 -6
  11. package/dist/managers/aiManager.d.ts.map +1 -1
  12. package/dist/managers/aiManager.js +122 -59
  13. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  14. package/dist/managers/backgroundTaskManager.js +28 -30
  15. package/dist/managers/hookManager.d.ts +16 -1
  16. package/dist/managers/hookManager.d.ts.map +1 -1
  17. package/dist/managers/hookManager.js +97 -8
  18. package/dist/managers/messageManager.d.ts +19 -4
  19. package/dist/managers/messageManager.d.ts.map +1 -1
  20. package/dist/managers/messageManager.js +63 -18
  21. package/dist/managers/pluginManager.d.ts.map +1 -1
  22. package/dist/managers/pluginManager.js +1 -1
  23. package/dist/prompts/index.d.ts +1 -1
  24. package/dist/prompts/index.d.ts.map +1 -1
  25. package/dist/prompts/index.js +1 -1
  26. package/dist/services/MarketplaceService.d.ts +42 -12
  27. package/dist/services/MarketplaceService.d.ts.map +1 -1
  28. package/dist/services/MarketplaceService.js +225 -105
  29. package/dist/services/aiService.d.ts +3 -3
  30. package/dist/services/aiService.d.ts.map +1 -1
  31. package/dist/services/aiService.js +7 -7
  32. package/dist/services/configurationService.d.ts +17 -1
  33. package/dist/services/configurationService.d.ts.map +1 -1
  34. package/dist/services/configurationService.js +104 -0
  35. package/dist/services/hook.d.ts.map +1 -1
  36. package/dist/services/hook.js +15 -0
  37. package/dist/services/initializationService.d.ts.map +1 -1
  38. package/dist/services/initializationService.js +24 -1
  39. package/dist/services/interactionService.js +1 -1
  40. package/dist/services/pluginLoader.d.ts.map +1 -1
  41. package/dist/services/pluginLoader.js +7 -1
  42. package/dist/services/session.d.ts +1 -1
  43. package/dist/services/session.js +7 -7
  44. package/dist/services/taskManager.d.ts +1 -1
  45. package/dist/services/taskManager.js +1 -1
  46. package/dist/types/configuration.d.ts +7 -0
  47. package/dist/types/configuration.d.ts.map +1 -1
  48. package/dist/types/core.d.ts +1 -1
  49. package/dist/types/core.d.ts.map +1 -1
  50. package/dist/types/hooks.d.ts +9 -1
  51. package/dist/types/hooks.d.ts.map +1 -1
  52. package/dist/types/hooks.js +2 -0
  53. package/dist/types/marketplace.d.ts +2 -0
  54. package/dist/types/marketplace.d.ts.map +1 -1
  55. package/dist/types/messaging.d.ts +3 -3
  56. package/dist/types/messaging.d.ts.map +1 -1
  57. package/dist/utils/convertMessagesForAPI.d.ts +1 -1
  58. package/dist/utils/convertMessagesForAPI.js +7 -7
  59. package/dist/utils/groupMessagesByApiRound.d.ts +1 -1
  60. package/dist/utils/groupMessagesByApiRound.js +6 -6
  61. package/dist/utils/messageOperations.d.ts.map +1 -1
  62. package/dist/utils/messageOperations.js +3 -3
  63. package/package.json +1 -1
  64. package/src/agent.ts +16 -3
  65. package/src/core/plugin.ts +13 -7
  66. package/src/managers/aiManager.ts +142 -63
  67. package/src/managers/backgroundTaskManager.ts +33 -42
  68. package/src/managers/hookManager.ts +125 -10
  69. package/src/managers/messageManager.ts +76 -22
  70. package/src/managers/pluginManager.ts +4 -1
  71. package/src/prompts/index.ts +1 -1
  72. package/src/services/MarketplaceService.ts +301 -111
  73. package/src/services/aiService.ts +11 -11
  74. package/src/services/configurationService.ts +131 -0
  75. package/src/services/hook.ts +17 -0
  76. package/src/services/initializationService.ts +33 -1
  77. package/src/services/interactionService.ts +1 -1
  78. package/src/services/pluginLoader.ts +7 -1
  79. package/src/services/session.ts +7 -7
  80. package/src/services/taskManager.ts +1 -1
  81. package/src/types/configuration.ts +8 -0
  82. package/src/types/core.ts +1 -1
  83. package/src/types/hooks.ts +16 -2
  84. package/src/types/marketplace.ts +2 -0
  85. package/src/types/messaging.ts +3 -3
  86. package/src/utils/convertMessagesForAPI.ts +8 -8
  87. package/src/utils/groupMessagesByApiRound.ts +6 -6
  88. package/src/utils/messageOperations.ts +3 -5
@@ -8,14 +8,21 @@ import {
8
8
  InstalledPlugin,
9
9
  InstalledPluginsRegistry,
10
10
  MarketplaceManifest,
11
+ MarketplaceSource,
11
12
  } from "../types/marketplace.js";
12
13
  import { GitService } from "./GitService.js";
14
+ import { ConfigurationService } from "./configurationService.js";
15
+ import type { MarketplaceConfig, Scope } from "../types/configuration.js";
16
+ import { logger } from "../utils/globalLogger.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; // Process exists, lock is valid
135
+ return false;
84
136
  } catch {
85
- return true; // Process doesn't exist, lock is stale
137
+ return true;
86
138
  }
87
139
  } catch {
88
- return true; // Can't read lock file, assume stale
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; // 60 seconds total
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 known marketplaces registry
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 getKnownMarketplaces(): Promise<KnownMarketplacesRegistry> {
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
- try {
165
- const content = await fs.readFile(this.knownMarketplacesPath, "utf-8");
166
- if (!content.trim()) {
167
- return {
168
- marketplaces: [
169
- {
170
- ...MarketplaceService.BUILTIN_MARKETPLACE,
171
- isBuiltin: true,
172
- },
173
- ],
174
- };
175
- }
176
- return JSON.parse(content);
177
- } catch (error) {
178
- console.error("Failed to load known marketplaces:", error);
179
- return { marketplaces: [] };
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
  */
@@ -238,7 +337,6 @@ export class MarketplaceService {
238
337
  const content = await fs.readFile(manifestPath, "utf-8");
239
338
  const manifest = JSON.parse(content);
240
339
 
241
- // Basic validation
242
340
  if (
243
341
  !manifest.name ||
244
342
  !manifest.plugins ||
@@ -253,25 +351,60 @@ export class MarketplaceService {
253
351
  /**
254
352
  * Resolves the local path for a marketplace
255
353
  */
256
- public getMarketplacePath(marketplace: KnownMarketplace): string {
257
- if (marketplace.source.source === "directory") {
258
- return marketplace.source.path;
259
- } else if (marketplace.source.source === "github") {
260
- return path.join(this.marketplacesDir, marketplace.source.repo);
354
+ public getMarketplacePath(source: MarketplaceSource): string {
355
+ if (source.source === "directory") {
356
+ return source.path;
357
+ } else if (source.source === "github") {
358
+ return path.join(this.marketplacesDir, source.repo);
261
359
  } else {
262
- // For general git, use a hash of the URL to avoid path issues
263
- const hash = crypto
264
- .createHash("md5")
265
- .update(marketplace.source.url)
266
- .digest("hex");
360
+ const hash = crypto.createHash("md5").update(source.url).digest("hex");
267
361
  return path.join(this.marketplacesDir, hash);
268
362
  }
269
363
  }
270
364
 
365
+ /**
366
+ * Builds a KnownMarketplace from a scoped config, enriched with cache metadata
367
+ */
368
+ private async buildMarketplaceEntry(
369
+ name: string,
370
+ config: MarketplaceConfig,
371
+ cache: KnownMarketplace | undefined,
372
+ declaredScope: "user" | "project" | "local" | "builtin",
373
+ ): Promise<KnownMarketplace> {
374
+ return {
375
+ name,
376
+ source: config.source,
377
+ autoUpdate: config.autoUpdate ?? cache?.autoUpdate,
378
+ lastUpdated: cache?.lastUpdated,
379
+ isBuiltin: name === MarketplaceService.BUILTIN_MARKETPLACE.name,
380
+ declaredScope,
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Finds which scope declared a marketplace (user, project, local, or builtin)
386
+ */
387
+ getMarketplaceDeclaringSource(name: string): Scope | "builtin" | null {
388
+ if (name === MarketplaceService.BUILTIN_MARKETPLACE.name) return "builtin";
389
+
390
+ const scopes: Scope[] = ["local", "project", "user"];
391
+ for (const scope of scopes) {
392
+ const scoped = this.configurationService.getScopedMarketplaces(
393
+ this.workdir,
394
+ scope,
395
+ );
396
+ if (scoped[name]) return scope;
397
+ }
398
+ return null;
399
+ }
400
+
271
401
  /**
272
402
  * Adds a new marketplace (local directory, GitHub repo, or Git URL)
273
403
  */
274
- async addMarketplace(input: string): Promise<KnownMarketplace> {
404
+ async addMarketplace(
405
+ input: string,
406
+ scope: Scope = "user",
407
+ ): Promise<KnownMarketplace> {
275
408
  return this.withLock(async () => {
276
409
  let marketplace: KnownMarketplace;
277
410
 
@@ -287,7 +420,6 @@ export class MarketplaceService {
287
420
  !path.isAbsolute(input) &&
288
421
  !input.startsWith("."))
289
422
  ) {
290
- // Git or GitHub repo
291
423
  let urlOrRepo = input;
292
424
  let ref: string | undefined;
293
425
 
@@ -295,11 +427,11 @@ export class MarketplaceService {
295
427
  [urlOrRepo, ref] = input.split("#");
296
428
  }
297
429
 
298
- const tempMarketplace: KnownMarketplace = isFullUrl
299
- ? { name: "", source: { source: "git", url: urlOrRepo, ref } }
300
- : { name: "", source: { source: "github", repo: urlOrRepo, ref } };
430
+ const tempSource: MarketplaceSource = isFullUrl
431
+ ? { source: "git", url: urlOrRepo, ref }
432
+ : { source: "github", repo: urlOrRepo, ref };
301
433
 
302
- const targetPath = this.getMarketplacePath(tempMarketplace);
434
+ const targetPath = this.getMarketplacePath(tempSource);
303
435
 
304
436
  if (!existsSync(targetPath)) {
305
437
  try {
@@ -329,7 +461,6 @@ export class MarketplaceService {
329
461
  lastUpdated: new Date().toISOString(),
330
462
  };
331
463
  } else {
332
- // Local directory format
333
464
  const absolutePath = path.resolve(input);
334
465
  let manifest: MarketplaceManifest;
335
466
  try {
@@ -348,54 +479,97 @@ export class MarketplaceService {
348
479
  };
349
480
  }
350
481
 
351
- const registry = await this.getKnownMarketplaces();
482
+ const config: MarketplaceConfig = {
483
+ source: marketplace.source,
484
+ autoUpdate: marketplace.autoUpdate,
485
+ };
352
486
 
353
- // Check if already exists
354
- const existingIndex = registry.marketplaces.findIndex(
355
- (m) => m.name === marketplace.name,
487
+ await this.configurationService.addMarketplaceToScope(
488
+ this.workdir,
489
+ scope,
490
+ marketplace.name,
491
+ config,
356
492
  );
357
- if (existingIndex >= 0) {
358
- registry.marketplaces[existingIndex] = marketplace;
359
- } else {
360
- registry.marketplaces.push(marketplace);
361
- }
362
493
 
363
- // Ensure builtin is included if we are creating the file for the first time
364
- // and it hasn't been explicitly removed yet.
365
- // (getKnownMarketplaces already handles the default injection)
494
+ // Update cache with metadata
495
+ await this.updateCacheMarketplace(marketplace.name, {
496
+ source: marketplace.source,
497
+ autoUpdate: marketplace.autoUpdate,
498
+ lastUpdated: marketplace.lastUpdated,
499
+ });
366
500
 
367
- await this.saveKnownMarketplaces(registry);
368
501
  return marketplace;
369
502
  });
370
503
  }
371
504
 
372
505
  /**
373
- * Lists all registered marketplaces
506
+ * Lists all registered marketplaces by combining scoped settings + built-in
374
507
  */
375
508
  async listMarketplaces(): Promise<KnownMarketplace[]> {
376
- const registry = await this.getKnownMarketplaces();
377
- return registry.marketplaces.map((m) => ({
378
- ...m,
379
- isBuiltin: m.name === MarketplaceService.BUILTIN_MARKETPLACE.name,
380
- }));
509
+ const scopedMarketplaces = this.configurationService.getMergedMarketplaces(
510
+ this.workdir,
511
+ );
512
+ const cacheRegistry = await this.getCacheRegistry();
513
+ const cacheMap = new Map(
514
+ (cacheRegistry?.marketplaces ?? []).map((m) => [m.name, m]),
515
+ );
516
+
517
+ const result: KnownMarketplace[] = [];
518
+
519
+ // Add built-in marketplace
520
+ result.push({
521
+ ...MarketplaceService.BUILTIN_MARKETPLACE,
522
+ isBuiltin: true,
523
+ declaredScope: "builtin",
524
+ });
525
+
526
+ // Add all scoped marketplaces (local overrides project overrides user)
527
+ for (const [name, config] of Object.entries(scopedMarketplaces)) {
528
+ if (name === MarketplaceService.BUILTIN_MARKETPLACE.name) continue;
529
+ const cache = cacheMap.get(name);
530
+ const declaredScope = this.getMarketplaceDeclaringSource(name) as
531
+ | "user"
532
+ | "project"
533
+ | "local";
534
+ result.push(
535
+ await this.buildMarketplaceEntry(name, config, cache, declaredScope),
536
+ );
537
+ }
538
+
539
+ // Add cache entries not yet in scoped settings (backwards compatibility)
540
+ for (const [name, cache] of cacheMap.entries()) {
541
+ if (name === MarketplaceService.BUILTIN_MARKETPLACE.name) continue;
542
+ if (scopedMarketplaces[name]) continue;
543
+ result.push({
544
+ ...cache,
545
+ isBuiltin: false,
546
+ declaredScope: cache.declaredScope ?? "user",
547
+ });
548
+ }
549
+
550
+ return result;
381
551
  }
382
552
 
383
553
  /**
384
- * Removes a marketplace by name
554
+ * Removes a marketplace by name from the specified scope
385
555
  */
386
- async removeMarketplace(name: string): Promise<void> {
556
+ async removeMarketplace(name: string, scope?: Scope): Promise<void> {
387
557
  return this.withLock(async () => {
388
- const registry = await this.getKnownMarketplaces();
389
- const initialCount = registry.marketplaces.length;
390
- registry.marketplaces = registry.marketplaces.filter(
391
- (m) => m.name !== name,
392
- );
558
+ const targetScope =
559
+ scope || this.getMarketplaceDeclaringSource(name) || "user";
393
560
 
394
- if (registry.marketplaces.length === initialCount) {
395
- throw new Error(`Marketplace ${name} not found`);
561
+ if (targetScope === "builtin") {
562
+ throw new Error("Cannot remove built-in marketplace");
396
563
  }
397
564
 
398
- await this.saveKnownMarketplaces(registry);
565
+ await this.configurationService.removeMarketplaceFromScope(
566
+ this.workdir,
567
+ targetScope,
568
+ name,
569
+ );
570
+
571
+ // Also remove from cache
572
+ await this.removeFromCache(name);
399
573
  });
400
574
  }
401
575
 
@@ -407,10 +581,10 @@ export class MarketplaceService {
407
581
  options?: { updatePlugins?: boolean },
408
582
  ): Promise<void> {
409
583
  return this.withLock(async () => {
410
- const registry = await this.getKnownMarketplaces();
584
+ const marketplaces = await this.listMarketplaces();
411
585
  const toUpdate = name
412
- ? registry.marketplaces.filter((m) => m.name === name)
413
- : registry.marketplaces;
586
+ ? marketplaces.filter((m) => m.name === name)
587
+ : marketplaces;
414
588
 
415
589
  if (name && toUpdate.length === 0) {
416
590
  throw new Error(`Marketplace ${name} not found`);
@@ -430,7 +604,7 @@ export class MarketplaceService {
430
604
  );
431
605
  continue;
432
606
  }
433
- const targetPath = this.getMarketplacePath(marketplace);
607
+ const targetPath = this.getMarketplacePath(marketplace.source);
434
608
  if (existsSync(targetPath)) {
435
609
  await this.gitService.pull(targetPath);
436
610
  } else {
@@ -447,13 +621,17 @@ export class MarketplaceService {
447
621
  );
448
622
  }
449
623
  }
450
- // For directory source, we just re-validate the manifest
451
624
  const manifest = await this.loadMarketplaceManifest(
452
- this.getMarketplacePath(marketplace),
625
+ this.getMarketplacePath(marketplace.source),
453
626
  );
454
627
 
455
628
  marketplace.lastUpdated = new Date().toISOString();
456
629
 
630
+ // Update cache metadata
631
+ await this.updateCacheMarketplace(marketplace.name, {
632
+ lastUpdated: marketplace.lastUpdated,
633
+ });
634
+
457
635
  if (options?.updatePlugins) {
458
636
  const installedRegistry = await this.getInstalledPlugins();
459
637
  const pluginsToUpdate = installedRegistry.plugins.filter(
@@ -464,7 +642,7 @@ export class MarketplaceService {
464
642
  (p) => p.name === plugin.name,
465
643
  );
466
644
  if (!pluginEntry) {
467
- console.warn(
645
+ logger.warn(
468
646
  `Plugin "${plugin.name}" no longer found in marketplace "${marketplace.name}". Uninstalling...`,
469
647
  );
470
648
  try {
@@ -505,8 +683,6 @@ export class MarketplaceService {
505
683
  `Some marketplaces failed to update:\n${errors.join("\n")}`,
506
684
  );
507
685
  }
508
-
509
- await this.saveKnownMarketplaces(registry);
510
686
  });
511
687
  }
512
688
 
@@ -515,17 +691,20 @@ export class MarketplaceService {
515
691
  */
516
692
  async autoUpdateAll(): Promise<void> {
517
693
  return this.withLock(async () => {
518
- const registry = await this.getKnownMarketplaces();
519
- const toAutoUpdate = registry.marketplaces.filter((m) => m.autoUpdate);
694
+ const scopedMarketplaces =
695
+ this.configurationService.getMergedMarketplaces(this.workdir);
696
+ const toAutoUpdate = Object.entries(scopedMarketplaces)
697
+ .filter(([, config]) => config.autoUpdate)
698
+ .map(([name]) => name);
520
699
 
521
- for (const marketplace of toAutoUpdate) {
700
+ for (const marketplaceName of toAutoUpdate) {
522
701
  try {
523
- await this.updateMarketplace(marketplace.name, {
702
+ await this.updateMarketplace(marketplaceName, {
524
703
  updatePlugins: true,
525
704
  });
526
705
  } catch (error) {
527
706
  console.error(
528
- `Auto-update failed for marketplace "${marketplace.name}":`,
707
+ `Auto-update failed for marketplace "${marketplaceName}":`,
529
708
  error,
530
709
  );
531
710
  }
@@ -538,13 +717,30 @@ export class MarketplaceService {
538
717
  */
539
718
  async toggleAutoUpdate(name: string, enabled: boolean): Promise<void> {
540
719
  return this.withLock(async () => {
541
- const registry = await this.getKnownMarketplaces();
542
- const marketplace = registry.marketplaces.find((m) => m.name === name);
543
- if (!marketplace) {
720
+ const declaringSource = this.getMarketplaceDeclaringSource(name);
721
+ if (!declaringSource || declaringSource === "builtin") {
722
+ throw new Error(`Marketplace ${name} not found`);
723
+ }
724
+
725
+ const scoped = this.configurationService.getScopedMarketplaces(
726
+ this.workdir,
727
+ declaringSource,
728
+ );
729
+ const config = scoped[name];
730
+ if (!config) {
544
731
  throw new Error(`Marketplace ${name} not found`);
545
732
  }
546
- marketplace.autoUpdate = enabled;
547
- await this.saveKnownMarketplaces(registry);
733
+
734
+ config.autoUpdate = enabled;
735
+ await this.configurationService.addMarketplaceToScope(
736
+ this.workdir,
737
+ declaringSource,
738
+ name,
739
+ config,
740
+ );
741
+
742
+ // Also update cache
743
+ await this.updateCacheMarketplace(name, { autoUpdate: enabled });
548
744
  });
549
745
  }
550
746
 
@@ -567,7 +763,7 @@ export class MarketplaceService {
567
763
  throw new Error(`Marketplace ${marketplaceName} not found`);
568
764
  }
569
765
 
570
- const marketplacePath = this.getMarketplacePath(marketplace);
766
+ const marketplacePath = this.getMarketplacePath(marketplace.source);
571
767
  const manifest = await this.loadMarketplaceManifest(marketplacePath);
572
768
  const pluginEntry = manifest.plugins.find((p) => p.name === pluginName);
573
769
  if (!pluginEntry) {
@@ -615,7 +811,6 @@ export class MarketplaceService {
615
811
  const pluginManifest = JSON.parse(pluginManifestContent);
616
812
  const version = pluginManifest.version || "1.0.0";
617
813
 
618
- // Atomic installation
619
814
  const tmpPluginDir = path.join(
620
815
  this.tmpDir,
621
816
  `${pluginName}-${Date.now()}`,
@@ -623,7 +818,7 @@ export class MarketplaceService {
623
818
  try {
624
819
  if (isGitSource) {
625
820
  await fs.rename(pluginSrcPath, tmpPluginDir);
626
- tempCloneDir = undefined; // Already moved
821
+ tempCloneDir = undefined;
627
822
  } else {
628
823
  await fs.cp(pluginSrcPath, tmpPluginDir, { recursive: true });
629
824
  }
@@ -665,14 +860,12 @@ export class MarketplaceService {
665
860
  await this.saveInstalledPlugins(installedRegistry);
666
861
  return installedPlugin;
667
862
  } catch (error) {
668
- // Cleanup tmp dir if it exists
669
863
  if (existsSync(tmpPluginDir)) {
670
864
  await fs.rm(tmpPluginDir, { recursive: true, force: true });
671
865
  }
672
866
  throw error;
673
867
  }
674
868
  } catch (error) {
675
- // Cleanup temp clone dir if it exists
676
869
  if (tempCloneDir && existsSync(tempCloneDir)) {
677
870
  await fs.rm(tempCloneDir, { recursive: true, force: true });
678
871
  }
@@ -712,16 +905,13 @@ export class MarketplaceService {
712
905
 
713
906
  const pluginToRemove = installedRegistry.plugins[pluginIndex];
714
907
 
715
- // Remove from registry first
716
908
  installedRegistry.plugins.splice(pluginIndex, 1);
717
909
  await this.saveInstalledPlugins(installedRegistry);
718
910
 
719
- // Check if any other project is still using this same cache path
720
911
  const isStillReferenced = installedRegistry.plugins.some(
721
912
  (p) => p.cachePath === pluginToRemove.cachePath,
722
913
  );
723
914
 
724
- // Only remove cached files if no other references exist
725
915
  if (!isStillReferenced && existsSync(pluginToRemove.cachePath)) {
726
916
  await fs.rm(pluginToRemove.cachePath, { recursive: true, force: true });
727
917
  }