wave-agent-sdk 0.11.5 → 0.11.7

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 (110) hide show
  1. package/builtin/skills/init/SKILL.md +2 -0
  2. package/builtin/skills/settings/SKILLS.md +3 -2
  3. package/builtin/skills/settings/SUBAGENTS.md +1 -3
  4. package/dist/agent.d.ts +6 -0
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +18 -1
  7. package/dist/constants/tools.d.ts +1 -1
  8. package/dist/constants/tools.d.ts.map +1 -1
  9. package/dist/constants/tools.js +1 -1
  10. package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
  11. package/dist/managers/MemoryRuleManager.js +1 -9
  12. package/dist/managers/aiManager.d.ts.map +1 -1
  13. package/dist/managers/aiManager.js +22 -3
  14. package/dist/managers/mcpManager.d.ts.map +1 -1
  15. package/dist/managers/mcpManager.js +32 -13
  16. package/dist/managers/messageManager.d.ts +13 -5
  17. package/dist/managers/messageManager.d.ts.map +1 -1
  18. package/dist/managers/messageManager.js +62 -34
  19. package/dist/managers/permissionManager.js +4 -4
  20. package/dist/managers/pluginManager.d.ts.map +1 -1
  21. package/dist/managers/pluginManager.js +4 -2
  22. package/dist/managers/slashCommandManager.d.ts +2 -0
  23. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  24. package/dist/managers/slashCommandManager.js +98 -4
  25. package/dist/managers/toolManager.d.ts.map +1 -1
  26. package/dist/managers/toolManager.js +8 -2
  27. package/dist/prompts/index.d.ts +2 -0
  28. package/dist/prompts/index.d.ts.map +1 -1
  29. package/dist/prompts/index.js +5 -0
  30. package/dist/services/GitService.d.ts +1 -0
  31. package/dist/services/GitService.d.ts.map +1 -1
  32. package/dist/services/GitService.js +16 -0
  33. package/dist/services/MarketplaceService.d.ts +7 -0
  34. package/dist/services/MarketplaceService.d.ts.map +1 -1
  35. package/dist/services/MarketplaceService.js +321 -252
  36. package/dist/services/aiService.d.ts +34 -0
  37. package/dist/services/aiService.d.ts.map +1 -1
  38. package/dist/services/aiService.js +124 -1
  39. package/dist/services/initializationService.d.ts.map +1 -1
  40. package/dist/services/initializationService.js +18 -0
  41. package/dist/tools/agentTool.js +3 -3
  42. package/dist/tools/bashTool.d.ts.map +1 -1
  43. package/dist/tools/bashTool.js +4 -4
  44. package/dist/tools/editTool.d.ts.map +1 -1
  45. package/dist/tools/editTool.js +2 -0
  46. package/dist/tools/globTool.d.ts.map +1 -1
  47. package/dist/tools/globTool.js +15 -3
  48. package/dist/tools/grepTool.d.ts.map +1 -1
  49. package/dist/tools/grepTool.js +38 -12
  50. package/dist/tools/readTool.d.ts.map +1 -1
  51. package/dist/tools/readTool.js +61 -0
  52. package/dist/tools/skillTool.js +2 -2
  53. package/dist/tools/types.d.ts +16 -0
  54. package/dist/tools/types.d.ts.map +1 -1
  55. package/dist/tools/webFetchTool.d.ts +3 -0
  56. package/dist/tools/webFetchTool.d.ts.map +1 -0
  57. package/dist/tools/webFetchTool.js +171 -0
  58. package/dist/tools/writeTool.d.ts.map +1 -1
  59. package/dist/tools/writeTool.js +2 -0
  60. package/dist/types/commands.d.ts +1 -1
  61. package/dist/types/commands.d.ts.map +1 -1
  62. package/dist/types/messaging.d.ts +1 -0
  63. package/dist/types/messaging.d.ts.map +1 -1
  64. package/dist/utils/bashParser.d.ts +20 -2
  65. package/dist/utils/bashParser.d.ts.map +1 -1
  66. package/dist/utils/bashParser.js +281 -146
  67. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  68. package/dist/utils/convertMessagesForAPI.js +7 -0
  69. package/dist/utils/fileUtils.d.ts +8 -0
  70. package/dist/utils/fileUtils.d.ts.map +1 -1
  71. package/dist/utils/fileUtils.js +52 -0
  72. package/dist/utils/messageOperations.d.ts +12 -3
  73. package/dist/utils/messageOperations.d.ts.map +1 -1
  74. package/dist/utils/messageOperations.js +77 -9
  75. package/package.json +4 -2
  76. package/src/agent.ts +19 -1
  77. package/src/constants/tools.ts +1 -1
  78. package/src/managers/MemoryRuleManager.ts +1 -10
  79. package/src/managers/aiManager.ts +23 -3
  80. package/src/managers/mcpManager.ts +37 -16
  81. package/src/managers/messageManager.ts +76 -38
  82. package/src/managers/permissionManager.ts +4 -4
  83. package/src/managers/pluginManager.ts +4 -2
  84. package/src/managers/slashCommandManager.ts +130 -4
  85. package/src/managers/toolManager.ts +11 -2
  86. package/src/prompts/index.ts +6 -0
  87. package/src/services/GitService.ts +20 -0
  88. package/src/services/MarketplaceService.ts +397 -324
  89. package/src/services/aiService.ts +197 -1
  90. package/src/services/initializationService.ts +38 -0
  91. package/src/tools/agentTool.ts +3 -3
  92. package/src/tools/bashTool.ts +3 -4
  93. package/src/tools/editTool.ts +3 -0
  94. package/src/tools/globTool.ts +16 -3
  95. package/src/tools/grepTool.ts +41 -13
  96. package/src/tools/readTool.ts +69 -0
  97. package/src/tools/skillTool.ts +2 -2
  98. package/src/tools/types.ts +13 -0
  99. package/src/tools/webFetchTool.ts +194 -0
  100. package/src/tools/writeTool.ts +3 -0
  101. package/src/types/commands.ts +1 -1
  102. package/src/types/messaging.ts +1 -0
  103. package/src/utils/bashParser.ts +316 -161
  104. package/src/utils/convertMessagesForAPI.ts +8 -0
  105. package/src/utils/fileUtils.ts +69 -0
  106. package/src/utils/messageOperations.ts +84 -9
  107. package/dist/tools/taskOutputTool.d.ts +0 -3
  108. package/dist/tools/taskOutputTool.d.ts.map +0 -1
  109. package/dist/tools/taskOutputTool.js +0 -198
  110. package/src/tools/taskOutputTool.ts +0 -222
@@ -18,9 +18,11 @@ import { GitService } from "./GitService.js";
18
18
  * and state management for installed plugins.
19
19
  */
20
20
  export class MarketplaceService {
21
+ private static isLockedInProcess = false;
21
22
  private pluginsDir: string;
22
23
  private knownMarketplacesPath: string;
23
24
  private installedPluginsPath: string;
25
+ private lockPath: string;
24
26
  private tmpDir: string;
25
27
  private cacheDir: string;
26
28
  private marketplacesDir: string;
@@ -44,6 +46,7 @@ export class MarketplaceService {
44
46
  this.pluginsDir,
45
47
  "installed_plugins.json",
46
48
  );
49
+ this.lockPath = path.join(this.pluginsDir, ".lock");
47
50
  this.tmpDir = path.join(this.pluginsDir, "tmp");
48
51
  this.cacheDir = path.join(this.pluginsDir, "cache");
49
52
  this.marketplacesDir = path.join(this.pluginsDir, "marketplaces");
@@ -65,6 +68,53 @@ export class MarketplaceService {
65
68
  );
66
69
  }
67
70
 
71
+ /**
72
+ * Acquires a file-based lock and executes the provided function.
73
+ * Supports re-entrancy within the same process.
74
+ */
75
+ private async withLock<T>(fn: () => Promise<T>): Promise<T> {
76
+ if (MarketplaceService.isLockedInProcess) {
77
+ return await fn();
78
+ }
79
+
80
+ let lockFd: Awaited<ReturnType<typeof fs.open>> | undefined;
81
+ const maxRetries = 600; // 60 seconds total
82
+ const retryDelay = 100;
83
+
84
+ for (let i = 0; i < maxRetries; i++) {
85
+ try {
86
+ lockFd = await fs.open(this.lockPath, "wx");
87
+ break;
88
+ } catch (error) {
89
+ if (
90
+ error &&
91
+ typeof error === "object" &&
92
+ "code" in error &&
93
+ error.code === "EEXIST"
94
+ ) {
95
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
96
+ continue;
97
+ }
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ if (!lockFd) {
103
+ throw new Error(
104
+ `Failed to acquire marketplace lock after ${maxRetries} retries. If no other wave-agent process is running, please delete ${this.lockPath}`,
105
+ );
106
+ }
107
+
108
+ MarketplaceService.isLockedInProcess = true;
109
+ try {
110
+ return await fn();
111
+ } finally {
112
+ MarketplaceService.isLockedInProcess = false;
113
+ await lockFd.close();
114
+ await fs.unlink(this.lockPath).catch(() => {});
115
+ }
116
+ }
117
+
68
118
  /**
69
119
  * Loads the known marketplaces registry
70
120
  */
@@ -123,10 +173,9 @@ export class MarketplaceService {
123
173
  async saveKnownMarketplaces(
124
174
  registry: KnownMarketplacesRegistry,
125
175
  ): Promise<void> {
126
- await fs.writeFile(
127
- this.knownMarketplacesPath,
128
- JSON.stringify(registry, null, 2),
129
- );
176
+ const tmpPath = `${this.knownMarketplacesPath}.tmp`;
177
+ await fs.writeFile(tmpPath, JSON.stringify(registry, null, 2));
178
+ await fs.rename(tmpPath, this.knownMarketplacesPath);
130
179
  }
131
180
 
132
181
  /**
@@ -135,10 +184,9 @@ export class MarketplaceService {
135
184
  async saveInstalledPlugins(
136
185
  registry: InstalledPluginsRegistry,
137
186
  ): Promise<void> {
138
- await fs.writeFile(
139
- this.installedPluginsPath,
140
- JSON.stringify(registry, null, 2),
141
- );
187
+ const tmpPath = `${this.installedPluginsPath}.tmp`;
188
+ await fs.writeFile(tmpPath, JSON.stringify(registry, null, 2));
189
+ await fs.rename(tmpPath, this.installedPluginsPath);
142
190
  }
143
191
 
144
192
  /**
@@ -192,97 +240,101 @@ export class MarketplaceService {
192
240
  * Adds a new marketplace (local directory, GitHub repo, or Git URL)
193
241
  */
194
242
  async addMarketplace(input: string): Promise<KnownMarketplace> {
195
- let marketplace: KnownMarketplace;
196
-
197
- const isFullUrl =
198
- input.startsWith("http://") ||
199
- input.startsWith("https://") ||
200
- input.startsWith("git@") ||
201
- input.startsWith("ssh://");
202
-
203
- if (
204
- isFullUrl ||
205
- (input.includes("/") && !path.isAbsolute(input) && !input.startsWith("."))
206
- ) {
207
- // Git or GitHub repo
208
- let urlOrRepo = input;
209
- let ref: string | undefined;
243
+ return this.withLock(async () => {
244
+ let marketplace: KnownMarketplace;
245
+
246
+ const isFullUrl =
247
+ input.startsWith("http://") ||
248
+ input.startsWith("https://") ||
249
+ input.startsWith("git@") ||
250
+ input.startsWith("ssh://");
251
+
252
+ if (
253
+ isFullUrl ||
254
+ (input.includes("/") &&
255
+ !path.isAbsolute(input) &&
256
+ !input.startsWith("."))
257
+ ) {
258
+ // Git or GitHub repo
259
+ let urlOrRepo = input;
260
+ let ref: string | undefined;
261
+
262
+ if (input.includes("#")) {
263
+ [urlOrRepo, ref] = input.split("#");
264
+ }
210
265
 
211
- if (input.includes("#")) {
212
- [urlOrRepo, ref] = input.split("#");
213
- }
266
+ const tempMarketplace: KnownMarketplace = isFullUrl
267
+ ? { name: "", source: { source: "git", url: urlOrRepo, ref } }
268
+ : { name: "", source: { source: "github", repo: urlOrRepo, ref } };
214
269
 
215
- const tempMarketplace: KnownMarketplace = isFullUrl
216
- ? { name: "", source: { source: "git", url: urlOrRepo, ref } }
217
- : { name: "", source: { source: "github", repo: urlOrRepo, ref } };
270
+ const targetPath = this.getMarketplacePath(tempMarketplace);
218
271
 
219
- const targetPath = this.getMarketplacePath(tempMarketplace);
272
+ if (!existsSync(targetPath)) {
273
+ try {
274
+ await this.gitService.clone(urlOrRepo, targetPath, ref);
275
+ } catch (error) {
276
+ throw new Error(
277
+ `Failed to add marketplace from Git: ${error instanceof Error ? error.message : String(error)}`,
278
+ );
279
+ }
280
+ }
220
281
 
221
- if (!existsSync(targetPath)) {
282
+ let manifest: MarketplaceManifest;
222
283
  try {
223
- await this.gitService.clone(urlOrRepo, targetPath, ref);
284
+ manifest = await this.loadMarketplaceManifest(targetPath);
224
285
  } catch (error) {
225
286
  throw new Error(
226
- `Failed to add marketplace from Git: ${error instanceof Error ? error.message : String(error)}`,
287
+ `Failed to load manifest from cloned repository: ${error instanceof Error ? error.message : String(error)}`,
227
288
  );
228
289
  }
229
- }
230
290
 
231
- let manifest: MarketplaceManifest;
232
- try {
233
- manifest = await this.loadMarketplaceManifest(targetPath);
234
- } catch (error) {
235
- throw new Error(
236
- `Failed to load manifest from cloned repository: ${error instanceof Error ? error.message : String(error)}`,
237
- );
238
- }
291
+ marketplace = {
292
+ name: manifest.name,
293
+ source: isFullUrl
294
+ ? { source: "git", url: urlOrRepo, ref }
295
+ : { source: "github", repo: urlOrRepo, ref },
296
+ autoUpdate: false,
297
+ lastUpdated: new Date().toISOString(),
298
+ };
299
+ } else {
300
+ // Local directory format
301
+ const absolutePath = path.resolve(input);
302
+ let manifest: MarketplaceManifest;
303
+ try {
304
+ manifest = await this.loadMarketplaceManifest(absolutePath);
305
+ } catch (error) {
306
+ throw new Error(
307
+ `Failed to load manifest from directory: ${error instanceof Error ? error.message : String(error)}`,
308
+ );
309
+ }
239
310
 
240
- marketplace = {
241
- name: manifest.name,
242
- source: isFullUrl
243
- ? { source: "git", url: urlOrRepo, ref }
244
- : { source: "github", repo: urlOrRepo, ref },
245
- autoUpdate: false,
246
- lastUpdated: new Date().toISOString(),
247
- };
248
- } else {
249
- // Local directory format
250
- const absolutePath = path.resolve(input);
251
- let manifest: MarketplaceManifest;
252
- try {
253
- manifest = await this.loadMarketplaceManifest(absolutePath);
254
- } catch (error) {
255
- throw new Error(
256
- `Failed to load manifest from directory: ${error instanceof Error ? error.message : String(error)}`,
257
- );
311
+ marketplace = {
312
+ name: manifest.name,
313
+ source: { source: "directory", path: absolutePath },
314
+ autoUpdate: false,
315
+ lastUpdated: new Date().toISOString(),
316
+ };
258
317
  }
259
318
 
260
- marketplace = {
261
- name: manifest.name,
262
- source: { source: "directory", path: absolutePath },
263
- autoUpdate: false,
264
- lastUpdated: new Date().toISOString(),
265
- };
266
- }
319
+ const registry = await this.getKnownMarketplaces();
267
320
 
268
- const registry = await this.getKnownMarketplaces();
269
-
270
- // Check if already exists
271
- const existingIndex = registry.marketplaces.findIndex(
272
- (m) => m.name === marketplace.name,
273
- );
274
- if (existingIndex >= 0) {
275
- registry.marketplaces[existingIndex] = marketplace;
276
- } else {
277
- registry.marketplaces.push(marketplace);
278
- }
321
+ // Check if already exists
322
+ const existingIndex = registry.marketplaces.findIndex(
323
+ (m) => m.name === marketplace.name,
324
+ );
325
+ if (existingIndex >= 0) {
326
+ registry.marketplaces[existingIndex] = marketplace;
327
+ } else {
328
+ registry.marketplaces.push(marketplace);
329
+ }
279
330
 
280
- // Ensure builtin is included if we are creating the file for the first time
281
- // and it hasn't been explicitly removed yet.
282
- // (getKnownMarketplaces already handles the default injection)
331
+ // Ensure builtin is included if we are creating the file for the first time
332
+ // and it hasn't been explicitly removed yet.
333
+ // (getKnownMarketplaces already handles the default injection)
283
334
 
284
- await this.saveKnownMarketplaces(registry);
285
- return marketplace;
335
+ await this.saveKnownMarketplaces(registry);
336
+ return marketplace;
337
+ });
286
338
  }
287
339
 
288
340
  /**
@@ -300,17 +352,19 @@ export class MarketplaceService {
300
352
  * Removes a marketplace by name
301
353
  */
302
354
  async removeMarketplace(name: string): Promise<void> {
303
- const registry = await this.getKnownMarketplaces();
304
- const initialCount = registry.marketplaces.length;
305
- registry.marketplaces = registry.marketplaces.filter(
306
- (m) => m.name !== name,
307
- );
355
+ return this.withLock(async () => {
356
+ const registry = await this.getKnownMarketplaces();
357
+ const initialCount = registry.marketplaces.length;
358
+ registry.marketplaces = registry.marketplaces.filter(
359
+ (m) => m.name !== name,
360
+ );
308
361
 
309
- if (registry.marketplaces.length === initialCount) {
310
- throw new Error(`Marketplace ${name} not found`);
311
- }
362
+ if (registry.marketplaces.length === initialCount) {
363
+ throw new Error(`Marketplace ${name} not found`);
364
+ }
312
365
 
313
- await this.saveKnownMarketplaces(registry);
366
+ await this.saveKnownMarketplaces(registry);
367
+ });
314
368
  }
315
369
 
316
370
  /**
@@ -320,138 +374,146 @@ export class MarketplaceService {
320
374
  name?: string,
321
375
  options?: { updatePlugins?: boolean },
322
376
  ): Promise<void> {
323
- const registry = await this.getKnownMarketplaces();
324
- const toUpdate = name
325
- ? registry.marketplaces.filter((m) => m.name === name)
326
- : registry.marketplaces;
327
-
328
- if (name && toUpdate.length === 0) {
329
- throw new Error(`Marketplace ${name} not found`);
330
- }
377
+ return this.withLock(async () => {
378
+ const registry = await this.getKnownMarketplaces();
379
+ const toUpdate = name
380
+ ? registry.marketplaces.filter((m) => m.name === name)
381
+ : registry.marketplaces;
382
+
383
+ if (name && toUpdate.length === 0) {
384
+ throw new Error(`Marketplace ${name} not found`);
385
+ }
331
386
 
332
- const isGitAvailable = await this.gitService.isGitAvailable();
333
- const errors: string[] = [];
334
- for (const marketplace of toUpdate) {
335
- try {
336
- if (
337
- marketplace.source.source === "github" ||
338
- marketplace.source.source === "git"
339
- ) {
340
- if (!isGitAvailable) {
341
- console.warn(
342
- `Skipping update for Git/GitHub marketplace "${marketplace.name}" because Git is not installed.`,
343
- );
344
- continue;
345
- }
346
- const targetPath = this.getMarketplacePath(marketplace);
347
- if (existsSync(targetPath)) {
348
- await this.gitService.pull(targetPath);
349
- } else {
350
- let url: string;
351
- if (marketplace.source.source === "github") {
352
- url = marketplace.source.repo;
387
+ const isGitAvailable = await this.gitService.isGitAvailable();
388
+ const errors: string[] = [];
389
+ for (const marketplace of toUpdate) {
390
+ try {
391
+ if (
392
+ marketplace.source.source === "github" ||
393
+ marketplace.source.source === "git"
394
+ ) {
395
+ if (!isGitAvailable) {
396
+ console.warn(
397
+ `Skipping update for Git/GitHub marketplace "${marketplace.name}" because Git is not installed.`,
398
+ );
399
+ continue;
400
+ }
401
+ const targetPath = this.getMarketplacePath(marketplace);
402
+ if (existsSync(targetPath)) {
403
+ await this.gitService.pull(targetPath);
353
404
  } else {
354
- url = marketplace.source.url;
405
+ let url: string;
406
+ if (marketplace.source.source === "github") {
407
+ url = marketplace.source.repo;
408
+ } else {
409
+ url = marketplace.source.url;
410
+ }
411
+ await this.gitService.clone(
412
+ url,
413
+ targetPath,
414
+ marketplace.source.ref,
415
+ );
355
416
  }
356
- await this.gitService.clone(
357
- url,
358
- targetPath,
359
- marketplace.source.ref,
360
- );
361
417
  }
362
- }
363
- // For directory source, we just re-validate the manifest
364
- const manifest = await this.loadMarketplaceManifest(
365
- this.getMarketplacePath(marketplace),
366
- );
418
+ // For directory source, we just re-validate the manifest
419
+ const manifest = await this.loadMarketplaceManifest(
420
+ this.getMarketplacePath(marketplace),
421
+ );
367
422
 
368
- marketplace.lastUpdated = new Date().toISOString();
423
+ marketplace.lastUpdated = new Date().toISOString();
369
424
 
370
- if (options?.updatePlugins) {
371
- const installedRegistry = await this.getInstalledPlugins();
372
- const pluginsToUpdate = installedRegistry.plugins.filter(
373
- (p) => p.marketplace === marketplace.name,
374
- );
375
- for (const plugin of pluginsToUpdate) {
376
- const pluginEntry = manifest.plugins.find(
377
- (p) => p.name === plugin.name,
425
+ if (options?.updatePlugins) {
426
+ const installedRegistry = await this.getInstalledPlugins();
427
+ const pluginsToUpdate = installedRegistry.plugins.filter(
428
+ (p) => p.marketplace === marketplace.name,
378
429
  );
379
- if (!pluginEntry) {
380
- console.warn(
381
- `Plugin "${plugin.name}" no longer found in marketplace "${marketplace.name}". Uninstalling...`,
430
+ for (const plugin of pluginsToUpdate) {
431
+ const pluginEntry = manifest.plugins.find(
432
+ (p) => p.name === plugin.name,
382
433
  );
434
+ if (!pluginEntry) {
435
+ console.warn(
436
+ `Plugin "${plugin.name}" no longer found in marketplace "${marketplace.name}". Uninstalling...`,
437
+ );
438
+ try {
439
+ await this.uninstallPlugin(
440
+ `${plugin.name}@${plugin.marketplace}`,
441
+ plugin.projectPath,
442
+ );
443
+ } catch (error) {
444
+ console.error(
445
+ `Failed to uninstall orphaned plugin "${plugin.name}" from marketplace "${marketplace.name}":`,
446
+ error,
447
+ );
448
+ }
449
+ continue;
450
+ }
383
451
  try {
384
- await this.uninstallPlugin(
452
+ await this.installPlugin(
385
453
  `${plugin.name}@${plugin.marketplace}`,
386
454
  plugin.projectPath,
387
455
  );
388
456
  } catch (error) {
389
457
  console.error(
390
- `Failed to uninstall orphaned plugin "${plugin.name}" from marketplace "${marketplace.name}":`,
458
+ `Failed to update plugin "${plugin.name}" from marketplace "${marketplace.name}":`,
391
459
  error,
392
460
  );
393
461
  }
394
- continue;
395
- }
396
- try {
397
- await this.installPlugin(
398
- `${plugin.name}@${plugin.marketplace}`,
399
- plugin.projectPath,
400
- );
401
- } catch (error) {
402
- console.error(
403
- `Failed to update plugin "${plugin.name}" from marketplace "${marketplace.name}":`,
404
- error,
405
- );
406
462
  }
407
463
  }
464
+ } catch (error) {
465
+ const msg = `Failed to update marketplace "${marketplace.name}": ${error instanceof Error ? error.message : String(error)}`;
466
+ console.error(msg);
467
+ errors.push(msg);
408
468
  }
409
- } catch (error) {
410
- const msg = `Failed to update marketplace "${marketplace.name}": ${error instanceof Error ? error.message : String(error)}`;
411
- console.error(msg);
412
- errors.push(msg);
413
469
  }
414
- }
415
470
 
416
- if (errors.length > 0) {
417
- throw new Error(
418
- `Some marketplaces failed to update:\n${errors.join("\n")}`,
419
- );
420
- }
471
+ if (errors.length > 0) {
472
+ throw new Error(
473
+ `Some marketplaces failed to update:\n${errors.join("\n")}`,
474
+ );
475
+ }
421
476
 
422
- await this.saveKnownMarketplaces(registry);
477
+ await this.saveKnownMarketplaces(registry);
478
+ });
423
479
  }
424
480
 
425
481
  /**
426
482
  * Automatically updates all marketplaces that have auto-update enabled
427
483
  */
428
484
  async autoUpdateAll(): Promise<void> {
429
- const registry = await this.getKnownMarketplaces();
430
- const toAutoUpdate = registry.marketplaces.filter((m) => m.autoUpdate);
485
+ return this.withLock(async () => {
486
+ const registry = await this.getKnownMarketplaces();
487
+ const toAutoUpdate = registry.marketplaces.filter((m) => m.autoUpdate);
431
488
 
432
- for (const marketplace of toAutoUpdate) {
433
- try {
434
- await this.updateMarketplace(marketplace.name, { updatePlugins: true });
435
- } catch (error) {
436
- console.error(
437
- `Auto-update failed for marketplace "${marketplace.name}":`,
438
- error,
439
- );
489
+ for (const marketplace of toAutoUpdate) {
490
+ try {
491
+ await this.updateMarketplace(marketplace.name, {
492
+ updatePlugins: true,
493
+ });
494
+ } catch (error) {
495
+ console.error(
496
+ `Auto-update failed for marketplace "${marketplace.name}":`,
497
+ error,
498
+ );
499
+ }
440
500
  }
441
- }
501
+ });
442
502
  }
443
503
 
444
504
  /**
445
505
  * Toggles auto-update for a marketplace
446
506
  */
447
507
  async toggleAutoUpdate(name: string, enabled: boolean): Promise<void> {
448
- const registry = await this.getKnownMarketplaces();
449
- const marketplace = registry.marketplaces.find((m) => m.name === name);
450
- if (!marketplace) {
451
- throw new Error(`Marketplace ${name} not found`);
452
- }
453
- marketplace.autoUpdate = enabled;
454
- await this.saveKnownMarketplaces(registry);
508
+ return this.withLock(async () => {
509
+ const registry = await this.getKnownMarketplaces();
510
+ const marketplace = registry.marketplaces.find((m) => m.name === name);
511
+ if (!marketplace) {
512
+ throw new Error(`Marketplace ${name} not found`);
513
+ }
514
+ marketplace.autoUpdate = enabled;
515
+ await this.saveKnownMarketplaces(registry);
516
+ });
455
517
  }
456
518
 
457
519
  /**
@@ -461,125 +523,132 @@ export class MarketplaceService {
461
523
  pluginAtMarketplace: string,
462
524
  projectPath?: string,
463
525
  ): Promise<InstalledPlugin> {
464
- const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
465
- if (!pluginName || !marketplaceName) {
466
- throw new Error("Invalid plugin format. Use name@marketplace");
467
- }
468
-
469
- const marketplaces = await this.listMarketplaces();
470
- const marketplace = marketplaces.find((m) => m.name === marketplaceName);
471
- if (!marketplace) {
472
- throw new Error(`Marketplace ${marketplaceName} not found`);
473
- }
474
-
475
- const marketplacePath = this.getMarketplacePath(marketplace);
476
- const manifest = await this.loadMarketplaceManifest(marketplacePath);
477
- const pluginEntry = manifest.plugins.find((p) => p.name === pluginName);
478
- if (!pluginEntry) {
479
- throw new Error(
480
- `Plugin ${pluginName} not found in marketplace ${marketplaceName}`,
481
- );
482
- }
526
+ return this.withLock(async () => {
527
+ const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
528
+ if (!pluginName || !marketplaceName) {
529
+ throw new Error("Invalid plugin format. Use name@marketplace");
530
+ }
483
531
 
484
- const isGitSource =
485
- pluginEntry.source.startsWith("http://") ||
486
- pluginEntry.source.startsWith("https://") ||
487
- pluginEntry.source.startsWith("git@") ||
488
- pluginEntry.source.startsWith("ssh://");
489
-
490
- let pluginSrcPath: string;
491
- let tempCloneDir: string | undefined;
492
-
493
- if (isGitSource) {
494
- tempCloneDir = path.join(this.tmpDir, `clone-${Date.now()}`);
495
- let url = pluginEntry.source;
496
- let ref: string | undefined;
497
- if (url.includes("#")) {
498
- [url, ref] = url.split("#");
532
+ const marketplaces = await this.listMarketplaces();
533
+ const marketplace = marketplaces.find((m) => m.name === marketplaceName);
534
+ if (!marketplace) {
535
+ throw new Error(`Marketplace ${marketplaceName} not found`);
499
536
  }
500
- await this.gitService.clone(url, tempCloneDir, ref);
501
- pluginSrcPath = tempCloneDir;
502
- } else {
503
- pluginSrcPath = path.resolve(marketplacePath, pluginEntry.source);
504
- }
505
537
 
506
- const pluginManifestPath = path.join(
507
- pluginSrcPath,
508
- ".wave-plugin",
509
- "plugin.json",
510
- );
511
- if (!existsSync(pluginManifestPath)) {
512
- if (tempCloneDir) {
513
- await fs.rm(tempCloneDir, { recursive: true, force: true });
538
+ const marketplacePath = this.getMarketplacePath(marketplace);
539
+ const manifest = await this.loadMarketplaceManifest(marketplacePath);
540
+ const pluginEntry = manifest.plugins.find((p) => p.name === pluginName);
541
+ if (!pluginEntry) {
542
+ throw new Error(
543
+ `Plugin ${pluginName} not found in marketplace ${marketplaceName}`,
544
+ );
514
545
  }
515
- throw new Error(`Plugin manifest not found at ${pluginManifestPath}`);
516
- }
517
546
 
518
- const pluginManifestContent = await fs.readFile(
519
- pluginManifestPath,
520
- "utf-8",
521
- );
522
- const pluginManifest = JSON.parse(pluginManifestContent);
523
- const version = pluginManifest.version || "1.0.0";
547
+ const isGitSource =
548
+ pluginEntry.source.startsWith("http://") ||
549
+ pluginEntry.source.startsWith("https://") ||
550
+ pluginEntry.source.startsWith("git@") ||
551
+ pluginEntry.source.startsWith("ssh://");
524
552
 
525
- // Atomic installation
526
- const tmpPluginDir = path.join(this.tmpDir, `${pluginName}-${Date.now()}`);
527
- try {
528
- if (isGitSource) {
529
- await fs.rename(pluginSrcPath, tmpPluginDir);
530
- tempCloneDir = undefined; // Already moved
531
- } else {
532
- await fs.cp(pluginSrcPath, tmpPluginDir, { recursive: true });
533
- }
553
+ let pluginSrcPath: string;
554
+ let tempCloneDir: string | undefined;
534
555
 
535
- const cachePath = path.join(
536
- this.cacheDir,
537
- marketplaceName,
538
- pluginName,
539
- version,
540
- );
541
- if (existsSync(cachePath)) {
542
- await fs.rm(cachePath, { recursive: true, force: true });
543
- }
544
- await fs.mkdir(path.dirname(cachePath), { recursive: true });
545
- await fs.rename(tmpPluginDir, cachePath);
556
+ try {
557
+ if (isGitSource) {
558
+ tempCloneDir = path.join(this.tmpDir, `clone-${Date.now()}`);
559
+ let url = pluginEntry.source;
560
+ let ref: string | undefined;
561
+ if (url.includes("#")) {
562
+ [url, ref] = url.split("#");
563
+ }
564
+ await this.gitService.clone(url, tempCloneDir, ref);
565
+ pluginSrcPath = tempCloneDir;
566
+ } else {
567
+ pluginSrcPath = path.resolve(marketplacePath, pluginEntry.source);
568
+ }
546
569
 
547
- const installedRegistry = await this.getInstalledPlugins();
548
- const existingIndex = installedRegistry.plugins.findIndex(
549
- (p) =>
550
- p.name === pluginName &&
551
- p.marketplace === marketplaceName &&
552
- p.projectPath === projectPath,
553
- );
570
+ const pluginManifestPath = path.join(
571
+ pluginSrcPath,
572
+ ".wave-plugin",
573
+ "plugin.json",
574
+ );
575
+ if (!existsSync(pluginManifestPath)) {
576
+ throw new Error(`Plugin manifest not found at ${pluginManifestPath}`);
577
+ }
554
578
 
555
- const installedPlugin: InstalledPlugin = {
556
- name: pluginName,
557
- marketplace: marketplaceName,
558
- version,
559
- cachePath,
560
- projectPath,
561
- };
579
+ const pluginManifestContent = await fs.readFile(
580
+ pluginManifestPath,
581
+ "utf-8",
582
+ );
583
+ const pluginManifest = JSON.parse(pluginManifestContent);
584
+ const version = pluginManifest.version || "1.0.0";
562
585
 
563
- if (existingIndex >= 0) {
564
- installedRegistry.plugins[existingIndex] = installedPlugin;
565
- } else {
566
- installedRegistry.plugins.push(installedPlugin);
567
- }
586
+ // Atomic installation
587
+ const tmpPluginDir = path.join(
588
+ this.tmpDir,
589
+ `${pluginName}-${Date.now()}`,
590
+ );
591
+ try {
592
+ if (isGitSource) {
593
+ await fs.rename(pluginSrcPath, tmpPluginDir);
594
+ tempCloneDir = undefined; // Already moved
595
+ } else {
596
+ await fs.cp(pluginSrcPath, tmpPluginDir, { recursive: true });
597
+ }
568
598
 
569
- await this.saveInstalledPlugins(installedRegistry);
570
- return installedPlugin;
571
- } catch (error) {
572
- // Cleanup tmp dirs if they exist
573
- if (existsSync(tmpPluginDir)) {
574
- await fs.rm(tmpPluginDir, { recursive: true, force: true });
575
- }
576
- if (tempCloneDir && existsSync(tempCloneDir)) {
577
- await fs.rm(tempCloneDir, { recursive: true, force: true });
599
+ const cachePath = path.join(
600
+ this.cacheDir,
601
+ marketplaceName,
602
+ pluginName,
603
+ version,
604
+ );
605
+ if (existsSync(cachePath)) {
606
+ await fs.rm(cachePath, { recursive: true, force: true });
607
+ }
608
+ await fs.mkdir(path.dirname(cachePath), { recursive: true });
609
+ await fs.rename(tmpPluginDir, cachePath);
610
+
611
+ const installedRegistry = await this.getInstalledPlugins();
612
+ const existingIndex = installedRegistry.plugins.findIndex(
613
+ (p) =>
614
+ p.name === pluginName &&
615
+ p.marketplace === marketplaceName &&
616
+ p.projectPath === projectPath,
617
+ );
618
+
619
+ const installedPlugin: InstalledPlugin = {
620
+ name: pluginName,
621
+ marketplace: marketplaceName,
622
+ version,
623
+ cachePath,
624
+ projectPath,
625
+ };
626
+
627
+ if (existingIndex >= 0) {
628
+ installedRegistry.plugins[existingIndex] = installedPlugin;
629
+ } else {
630
+ installedRegistry.plugins.push(installedPlugin);
631
+ }
632
+
633
+ await this.saveInstalledPlugins(installedRegistry);
634
+ return installedPlugin;
635
+ } catch (error) {
636
+ // Cleanup tmp dir if it exists
637
+ if (existsSync(tmpPluginDir)) {
638
+ await fs.rm(tmpPluginDir, { recursive: true, force: true });
639
+ }
640
+ throw error;
641
+ }
642
+ } catch (error) {
643
+ // Cleanup temp clone dir if it exists
644
+ if (tempCloneDir && existsSync(tempCloneDir)) {
645
+ await fs.rm(tempCloneDir, { recursive: true, force: true });
646
+ }
647
+ throw new Error(
648
+ `Failed to install plugin ${pluginName}: ${error instanceof Error ? error.message : String(error)}`,
649
+ );
578
650
  }
579
- throw new Error(
580
- `Failed to install plugin ${pluginName}: ${error instanceof Error ? error.message : String(error)}`,
581
- );
582
- }
651
+ });
583
652
  }
584
653
 
585
654
  /**
@@ -589,47 +658,51 @@ export class MarketplaceService {
589
658
  pluginAtMarketplace: string,
590
659
  projectPath?: string,
591
660
  ): Promise<void> {
592
- const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
593
- if (!pluginName || !marketplaceName) {
594
- throw new Error("Invalid plugin format. Use name@marketplace");
595
- }
596
-
597
- const installedRegistry = await this.getInstalledPlugins();
598
- const pluginIndex = installedRegistry.plugins.findIndex(
599
- (p) =>
600
- p.name === pluginName &&
601
- p.marketplace === marketplaceName &&
602
- p.projectPath === projectPath,
603
- );
661
+ return this.withLock(async () => {
662
+ const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
663
+ if (!pluginName || !marketplaceName) {
664
+ throw new Error("Invalid plugin format. Use name@marketplace");
665
+ }
604
666
 
605
- if (pluginIndex === -1) {
606
- throw new Error(
607
- `Plugin ${pluginName}@${marketplaceName} is not installed${projectPath ? ` for project ${projectPath}` : ""}`,
667
+ const installedRegistry = await this.getInstalledPlugins();
668
+ const pluginIndex = installedRegistry.plugins.findIndex(
669
+ (p) =>
670
+ p.name === pluginName &&
671
+ p.marketplace === marketplaceName &&
672
+ p.projectPath === projectPath,
608
673
  );
609
- }
610
674
 
611
- const pluginToRemove = installedRegistry.plugins[pluginIndex];
675
+ if (pluginIndex === -1) {
676
+ throw new Error(
677
+ `Plugin ${pluginName}@${marketplaceName} is not installed${projectPath ? ` for project ${projectPath}` : ""}`,
678
+ );
679
+ }
680
+
681
+ const pluginToRemove = installedRegistry.plugins[pluginIndex];
612
682
 
613
- // Remove from registry first
614
- installedRegistry.plugins.splice(pluginIndex, 1);
615
- await this.saveInstalledPlugins(installedRegistry);
683
+ // Remove from registry first
684
+ installedRegistry.plugins.splice(pluginIndex, 1);
685
+ await this.saveInstalledPlugins(installedRegistry);
616
686
 
617
- // Check if any other project is still using this same cache path
618
- const isStillReferenced = installedRegistry.plugins.some(
619
- (p) => p.cachePath === pluginToRemove.cachePath,
620
- );
687
+ // Check if any other project is still using this same cache path
688
+ const isStillReferenced = installedRegistry.plugins.some(
689
+ (p) => p.cachePath === pluginToRemove.cachePath,
690
+ );
621
691
 
622
- // Only remove cached files if no other references exist
623
- if (!isStillReferenced && existsSync(pluginToRemove.cachePath)) {
624
- await fs.rm(pluginToRemove.cachePath, { recursive: true, force: true });
625
- }
692
+ // Only remove cached files if no other references exist
693
+ if (!isStillReferenced && existsSync(pluginToRemove.cachePath)) {
694
+ await fs.rm(pluginToRemove.cachePath, { recursive: true, force: true });
695
+ }
696
+ });
626
697
  }
627
698
 
628
699
  /**
629
700
  * Updates a plugin (uninstall followed by install)
630
701
  */
631
702
  async updatePlugin(pluginAtMarketplace: string): Promise<InstalledPlugin> {
632
- await this.uninstallPlugin(pluginAtMarketplace);
633
- return this.installPlugin(pluginAtMarketplace);
703
+ return this.withLock(async () => {
704
+ await this.uninstallPlugin(pluginAtMarketplace);
705
+ return this.installPlugin(pluginAtMarketplace);
706
+ });
634
707
  }
635
708
  }