wave-agent-sdk 0.11.6 → 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.
- package/builtin/skills/init/SKILL.md +2 -0
- package/builtin/skills/settings/SKILLS.md +3 -2
- package/builtin/skills/settings/SUBAGENTS.md +1 -3
- package/dist/agent.d.ts +6 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +18 -1
- package/dist/constants/tools.d.ts +1 -1
- package/dist/constants/tools.d.ts.map +1 -1
- package/dist/constants/tools.js +1 -1
- package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
- package/dist/managers/MemoryRuleManager.js +1 -9
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +22 -3
- package/dist/managers/messageManager.d.ts +13 -5
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +62 -34
- package/dist/managers/pluginManager.d.ts.map +1 -1
- package/dist/managers/pluginManager.js +4 -2
- package/dist/managers/slashCommandManager.d.ts +2 -0
- package/dist/managers/slashCommandManager.d.ts.map +1 -1
- package/dist/managers/slashCommandManager.js +98 -4
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +8 -2
- package/dist/prompts/index.d.ts +2 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +5 -0
- package/dist/services/GitService.d.ts +1 -0
- package/dist/services/GitService.d.ts.map +1 -1
- package/dist/services/GitService.js +16 -0
- package/dist/services/MarketplaceService.d.ts +7 -0
- package/dist/services/MarketplaceService.d.ts.map +1 -1
- package/dist/services/MarketplaceService.js +321 -252
- package/dist/services/aiService.d.ts +34 -0
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +124 -1
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +18 -0
- package/dist/tools/agentTool.js +3 -3
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +4 -4
- package/dist/tools/editTool.d.ts.map +1 -1
- package/dist/tools/editTool.js +2 -0
- package/dist/tools/globTool.d.ts.map +1 -1
- package/dist/tools/globTool.js +15 -3
- package/dist/tools/grepTool.d.ts.map +1 -1
- package/dist/tools/grepTool.js +38 -12
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +61 -0
- package/dist/tools/skillTool.js +2 -2
- package/dist/tools/types.d.ts +16 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/webFetchTool.d.ts +3 -0
- package/dist/tools/webFetchTool.d.ts.map +1 -0
- package/dist/tools/webFetchTool.js +171 -0
- package/dist/tools/writeTool.d.ts.map +1 -1
- package/dist/tools/writeTool.js +2 -0
- package/dist/types/commands.d.ts +1 -1
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +1 -0
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/utils/bashParser.d.ts +14 -0
- package/dist/utils/bashParser.d.ts.map +1 -1
- package/dist/utils/bashParser.js +243 -142
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +7 -0
- package/dist/utils/fileUtils.d.ts +8 -0
- package/dist/utils/fileUtils.d.ts.map +1 -1
- package/dist/utils/fileUtils.js +52 -0
- package/dist/utils/messageOperations.d.ts +12 -3
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +77 -9
- package/package.json +4 -2
- package/src/agent.ts +19 -1
- package/src/constants/tools.ts +1 -1
- package/src/managers/MemoryRuleManager.ts +1 -10
- package/src/managers/aiManager.ts +23 -3
- package/src/managers/messageManager.ts +76 -38
- package/src/managers/pluginManager.ts +4 -2
- package/src/managers/slashCommandManager.ts +130 -4
- package/src/managers/toolManager.ts +11 -2
- package/src/prompts/index.ts +6 -0
- package/src/services/GitService.ts +20 -0
- package/src/services/MarketplaceService.ts +397 -324
- package/src/services/aiService.ts +197 -1
- package/src/services/initializationService.ts +38 -0
- package/src/tools/agentTool.ts +3 -3
- package/src/tools/bashTool.ts +3 -4
- package/src/tools/editTool.ts +3 -0
- package/src/tools/globTool.ts +16 -3
- package/src/tools/grepTool.ts +41 -13
- package/src/tools/readTool.ts +69 -0
- package/src/tools/skillTool.ts +2 -2
- package/src/tools/types.ts +13 -0
- package/src/tools/webFetchTool.ts +194 -0
- package/src/tools/writeTool.ts +3 -0
- package/src/types/commands.ts +1 -1
- package/src/types/messaging.ts +1 -0
- package/src/utils/bashParser.ts +268 -157
- package/src/utils/convertMessagesForAPI.ts +8 -0
- package/src/utils/fileUtils.ts +69 -0
- package/src/utils/messageOperations.ts +84 -9
- package/dist/tools/taskOutputTool.d.ts +0 -3
- package/dist/tools/taskOutputTool.d.ts.map +0 -1
- package/dist/tools/taskOutputTool.js +0 -198
- package/src/tools/taskOutputTool.ts +0 -222
|
@@ -14,6 +14,7 @@ export class MarketplaceService {
|
|
|
14
14
|
this.pluginsDir = getPluginsDir();
|
|
15
15
|
this.knownMarketplacesPath = path.join(this.pluginsDir, "known_marketplaces.json");
|
|
16
16
|
this.installedPluginsPath = path.join(this.pluginsDir, "installed_plugins.json");
|
|
17
|
+
this.lockPath = path.join(this.pluginsDir, ".lock");
|
|
17
18
|
this.tmpDir = path.join(this.pluginsDir, "tmp");
|
|
18
19
|
this.cacheDir = path.join(this.pluginsDir, "cache");
|
|
19
20
|
this.marketplacesDir = path.join(this.pluginsDir, "marketplaces");
|
|
@@ -30,6 +31,46 @@ export class MarketplaceService {
|
|
|
30
31
|
}
|
|
31
32
|
});
|
|
32
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Acquires a file-based lock and executes the provided function.
|
|
36
|
+
* Supports re-entrancy within the same process.
|
|
37
|
+
*/
|
|
38
|
+
async withLock(fn) {
|
|
39
|
+
if (MarketplaceService.isLockedInProcess) {
|
|
40
|
+
return await fn();
|
|
41
|
+
}
|
|
42
|
+
let lockFd;
|
|
43
|
+
const maxRetries = 600; // 60 seconds total
|
|
44
|
+
const retryDelay = 100;
|
|
45
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
46
|
+
try {
|
|
47
|
+
lockFd = await fs.open(this.lockPath, "wx");
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error &&
|
|
52
|
+
typeof error === "object" &&
|
|
53
|
+
"code" in error &&
|
|
54
|
+
error.code === "EEXIST") {
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!lockFd) {
|
|
62
|
+
throw new Error(`Failed to acquire marketplace lock after ${maxRetries} retries. If no other wave-agent process is running, please delete ${this.lockPath}`);
|
|
63
|
+
}
|
|
64
|
+
MarketplaceService.isLockedInProcess = true;
|
|
65
|
+
try {
|
|
66
|
+
return await fn();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
MarketplaceService.isLockedInProcess = false;
|
|
70
|
+
await lockFd.close();
|
|
71
|
+
await fs.unlink(this.lockPath).catch(() => { });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
33
74
|
/**
|
|
34
75
|
* Loads the known marketplaces registry
|
|
35
76
|
*/
|
|
@@ -86,13 +127,17 @@ export class MarketplaceService {
|
|
|
86
127
|
* Saves the known marketplaces registry
|
|
87
128
|
*/
|
|
88
129
|
async saveKnownMarketplaces(registry) {
|
|
89
|
-
|
|
130
|
+
const tmpPath = `${this.knownMarketplacesPath}.tmp`;
|
|
131
|
+
await fs.writeFile(tmpPath, JSON.stringify(registry, null, 2));
|
|
132
|
+
await fs.rename(tmpPath, this.knownMarketplacesPath);
|
|
90
133
|
}
|
|
91
134
|
/**
|
|
92
135
|
* Saves the installed plugins registry
|
|
93
136
|
*/
|
|
94
137
|
async saveInstalledPlugins(registry) {
|
|
95
|
-
|
|
138
|
+
const tmpPath = `${this.installedPluginsPath}.tmp`;
|
|
139
|
+
await fs.writeFile(tmpPath, JSON.stringify(registry, null, 2));
|
|
140
|
+
await fs.rename(tmpPath, this.installedPluginsPath);
|
|
96
141
|
}
|
|
97
142
|
/**
|
|
98
143
|
* Loads a marketplace manifest from a local path
|
|
@@ -135,78 +180,82 @@ export class MarketplaceService {
|
|
|
135
180
|
* Adds a new marketplace (local directory, GitHub repo, or Git URL)
|
|
136
181
|
*/
|
|
137
182
|
async addMarketplace(input) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
input.startsWith("
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
183
|
+
return this.withLock(async () => {
|
|
184
|
+
let marketplace;
|
|
185
|
+
const isFullUrl = input.startsWith("http://") ||
|
|
186
|
+
input.startsWith("https://") ||
|
|
187
|
+
input.startsWith("git@") ||
|
|
188
|
+
input.startsWith("ssh://");
|
|
189
|
+
if (isFullUrl ||
|
|
190
|
+
(input.includes("/") &&
|
|
191
|
+
!path.isAbsolute(input) &&
|
|
192
|
+
!input.startsWith("."))) {
|
|
193
|
+
// Git or GitHub repo
|
|
194
|
+
let urlOrRepo = input;
|
|
195
|
+
let ref;
|
|
196
|
+
if (input.includes("#")) {
|
|
197
|
+
[urlOrRepo, ref] = input.split("#");
|
|
198
|
+
}
|
|
199
|
+
const tempMarketplace = isFullUrl
|
|
200
|
+
? { name: "", source: { source: "git", url: urlOrRepo, ref } }
|
|
201
|
+
: { name: "", source: { source: "github", repo: urlOrRepo, ref } };
|
|
202
|
+
const targetPath = this.getMarketplacePath(tempMarketplace);
|
|
203
|
+
if (!existsSync(targetPath)) {
|
|
204
|
+
try {
|
|
205
|
+
await this.gitService.clone(urlOrRepo, targetPath, ref);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
throw new Error(`Failed to add marketplace from Git: ${error instanceof Error ? error.message : String(error)}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
let manifest;
|
|
156
212
|
try {
|
|
157
|
-
await this.
|
|
213
|
+
manifest = await this.loadMarketplaceManifest(targetPath);
|
|
158
214
|
}
|
|
159
215
|
catch (error) {
|
|
160
|
-
throw new Error(`Failed to
|
|
216
|
+
throw new Error(`Failed to load manifest from cloned repository: ${error instanceof Error ? error.message : String(error)}`);
|
|
161
217
|
}
|
|
218
|
+
marketplace = {
|
|
219
|
+
name: manifest.name,
|
|
220
|
+
source: isFullUrl
|
|
221
|
+
? { source: "git", url: urlOrRepo, ref }
|
|
222
|
+
: { source: "github", repo: urlOrRepo, ref },
|
|
223
|
+
autoUpdate: false,
|
|
224
|
+
lastUpdated: new Date().toISOString(),
|
|
225
|
+
};
|
|
162
226
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
227
|
+
else {
|
|
228
|
+
// Local directory format
|
|
229
|
+
const absolutePath = path.resolve(input);
|
|
230
|
+
let manifest;
|
|
231
|
+
try {
|
|
232
|
+
manifest = await this.loadMarketplaceManifest(absolutePath);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
throw new Error(`Failed to load manifest from directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
236
|
+
}
|
|
237
|
+
marketplace = {
|
|
238
|
+
name: manifest.name,
|
|
239
|
+
source: { source: "directory", path: absolutePath },
|
|
240
|
+
autoUpdate: false,
|
|
241
|
+
lastUpdated: new Date().toISOString(),
|
|
242
|
+
};
|
|
169
243
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
autoUpdate: false,
|
|
176
|
-
lastUpdated: new Date().toISOString(),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
// Local directory format
|
|
181
|
-
const absolutePath = path.resolve(input);
|
|
182
|
-
let manifest;
|
|
183
|
-
try {
|
|
184
|
-
manifest = await this.loadMarketplaceManifest(absolutePath);
|
|
244
|
+
const registry = await this.getKnownMarketplaces();
|
|
245
|
+
// Check if already exists
|
|
246
|
+
const existingIndex = registry.marketplaces.findIndex((m) => m.name === marketplace.name);
|
|
247
|
+
if (existingIndex >= 0) {
|
|
248
|
+
registry.marketplaces[existingIndex] = marketplace;
|
|
185
249
|
}
|
|
186
|
-
|
|
187
|
-
|
|
250
|
+
else {
|
|
251
|
+
registry.marketplaces.push(marketplace);
|
|
188
252
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
const registry = await this.getKnownMarketplaces();
|
|
197
|
-
// Check if already exists
|
|
198
|
-
const existingIndex = registry.marketplaces.findIndex((m) => m.name === marketplace.name);
|
|
199
|
-
if (existingIndex >= 0) {
|
|
200
|
-
registry.marketplaces[existingIndex] = marketplace;
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
registry.marketplaces.push(marketplace);
|
|
204
|
-
}
|
|
205
|
-
// Ensure builtin is included if we are creating the file for the first time
|
|
206
|
-
// and it hasn't been explicitly removed yet.
|
|
207
|
-
// (getKnownMarketplaces already handles the default injection)
|
|
208
|
-
await this.saveKnownMarketplaces(registry);
|
|
209
|
-
return marketplace;
|
|
253
|
+
// Ensure builtin is included if we are creating the file for the first time
|
|
254
|
+
// and it hasn't been explicitly removed yet.
|
|
255
|
+
// (getKnownMarketplaces already handles the default injection)
|
|
256
|
+
await this.saveKnownMarketplaces(registry);
|
|
257
|
+
return marketplace;
|
|
258
|
+
});
|
|
210
259
|
}
|
|
211
260
|
/**
|
|
212
261
|
* Lists all registered marketplaces
|
|
@@ -222,244 +271,264 @@ export class MarketplaceService {
|
|
|
222
271
|
* Removes a marketplace by name
|
|
223
272
|
*/
|
|
224
273
|
async removeMarketplace(name) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
274
|
+
return this.withLock(async () => {
|
|
275
|
+
const registry = await this.getKnownMarketplaces();
|
|
276
|
+
const initialCount = registry.marketplaces.length;
|
|
277
|
+
registry.marketplaces = registry.marketplaces.filter((m) => m.name !== name);
|
|
278
|
+
if (registry.marketplaces.length === initialCount) {
|
|
279
|
+
throw new Error(`Marketplace ${name} not found`);
|
|
280
|
+
}
|
|
281
|
+
await this.saveKnownMarketplaces(registry);
|
|
282
|
+
});
|
|
232
283
|
}
|
|
233
284
|
/**
|
|
234
285
|
* Updates a specific marketplace or all marketplaces
|
|
235
286
|
*/
|
|
236
287
|
async updateMarketplace(name, options) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
marketplace.source.source === "
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
else {
|
|
259
|
-
let url;
|
|
260
|
-
if (marketplace.source.source === "github") {
|
|
261
|
-
url = marketplace.source.repo;
|
|
288
|
+
return this.withLock(async () => {
|
|
289
|
+
const registry = await this.getKnownMarketplaces();
|
|
290
|
+
const toUpdate = name
|
|
291
|
+
? registry.marketplaces.filter((m) => m.name === name)
|
|
292
|
+
: registry.marketplaces;
|
|
293
|
+
if (name && toUpdate.length === 0) {
|
|
294
|
+
throw new Error(`Marketplace ${name} not found`);
|
|
295
|
+
}
|
|
296
|
+
const isGitAvailable = await this.gitService.isGitAvailable();
|
|
297
|
+
const errors = [];
|
|
298
|
+
for (const marketplace of toUpdate) {
|
|
299
|
+
try {
|
|
300
|
+
if (marketplace.source.source === "github" ||
|
|
301
|
+
marketplace.source.source === "git") {
|
|
302
|
+
if (!isGitAvailable) {
|
|
303
|
+
console.warn(`Skipping update for Git/GitHub marketplace "${marketplace.name}" because Git is not installed.`);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const targetPath = this.getMarketplacePath(marketplace);
|
|
307
|
+
if (existsSync(targetPath)) {
|
|
308
|
+
await this.gitService.pull(targetPath);
|
|
262
309
|
}
|
|
263
310
|
else {
|
|
264
|
-
|
|
311
|
+
let url;
|
|
312
|
+
if (marketplace.source.source === "github") {
|
|
313
|
+
url = marketplace.source.repo;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
url = marketplace.source.url;
|
|
317
|
+
}
|
|
318
|
+
await this.gitService.clone(url, targetPath, marketplace.source.ref);
|
|
265
319
|
}
|
|
266
|
-
await this.gitService.clone(url, targetPath, marketplace.source.ref);
|
|
267
320
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
321
|
+
// For directory source, we just re-validate the manifest
|
|
322
|
+
const manifest = await this.loadMarketplaceManifest(this.getMarketplacePath(marketplace));
|
|
323
|
+
marketplace.lastUpdated = new Date().toISOString();
|
|
324
|
+
if (options?.updatePlugins) {
|
|
325
|
+
const installedRegistry = await this.getInstalledPlugins();
|
|
326
|
+
const pluginsToUpdate = installedRegistry.plugins.filter((p) => p.marketplace === marketplace.name);
|
|
327
|
+
for (const plugin of pluginsToUpdate) {
|
|
328
|
+
const pluginEntry = manifest.plugins.find((p) => p.name === plugin.name);
|
|
329
|
+
if (!pluginEntry) {
|
|
330
|
+
console.warn(`Plugin "${plugin.name}" no longer found in marketplace "${marketplace.name}". Uninstalling...`);
|
|
331
|
+
try {
|
|
332
|
+
await this.uninstallPlugin(`${plugin.name}@${plugin.marketplace}`, plugin.projectPath);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
console.error(`Failed to uninstall orphaned plugin "${plugin.name}" from marketplace "${marketplace.name}":`, error);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
279
339
|
try {
|
|
280
|
-
await this.
|
|
340
|
+
await this.installPlugin(`${plugin.name}@${plugin.marketplace}`, plugin.projectPath);
|
|
281
341
|
}
|
|
282
342
|
catch (error) {
|
|
283
|
-
console.error(`Failed to
|
|
343
|
+
console.error(`Failed to update plugin "${plugin.name}" from marketplace "${marketplace.name}":`, error);
|
|
284
344
|
}
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
try {
|
|
288
|
-
await this.installPlugin(`${plugin.name}@${plugin.marketplace}`, plugin.projectPath);
|
|
289
|
-
}
|
|
290
|
-
catch (error) {
|
|
291
|
-
console.error(`Failed to update plugin "${plugin.name}" from marketplace "${marketplace.name}":`, error);
|
|
292
345
|
}
|
|
293
346
|
}
|
|
294
347
|
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
const msg = `Failed to update marketplace "${marketplace.name}": ${error instanceof Error ? error.message : String(error)}`;
|
|
350
|
+
console.error(msg);
|
|
351
|
+
errors.push(msg);
|
|
352
|
+
}
|
|
295
353
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
console.error(msg);
|
|
299
|
-
errors.push(msg);
|
|
354
|
+
if (errors.length > 0) {
|
|
355
|
+
throw new Error(`Some marketplaces failed to update:\n${errors.join("\n")}`);
|
|
300
356
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
throw new Error(`Some marketplaces failed to update:\n${errors.join("\n")}`);
|
|
304
|
-
}
|
|
305
|
-
await this.saveKnownMarketplaces(registry);
|
|
357
|
+
await this.saveKnownMarketplaces(registry);
|
|
358
|
+
});
|
|
306
359
|
}
|
|
307
360
|
/**
|
|
308
361
|
* Automatically updates all marketplaces that have auto-update enabled
|
|
309
362
|
*/
|
|
310
363
|
async autoUpdateAll() {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
364
|
+
return this.withLock(async () => {
|
|
365
|
+
const registry = await this.getKnownMarketplaces();
|
|
366
|
+
const toAutoUpdate = registry.marketplaces.filter((m) => m.autoUpdate);
|
|
367
|
+
for (const marketplace of toAutoUpdate) {
|
|
368
|
+
try {
|
|
369
|
+
await this.updateMarketplace(marketplace.name, {
|
|
370
|
+
updatePlugins: true,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
console.error(`Auto-update failed for marketplace "${marketplace.name}":`, error);
|
|
375
|
+
}
|
|
319
376
|
}
|
|
320
|
-
}
|
|
377
|
+
});
|
|
321
378
|
}
|
|
322
379
|
/**
|
|
323
380
|
* Toggles auto-update for a marketplace
|
|
324
381
|
*/
|
|
325
382
|
async toggleAutoUpdate(name, enabled) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
383
|
+
return this.withLock(async () => {
|
|
384
|
+
const registry = await this.getKnownMarketplaces();
|
|
385
|
+
const marketplace = registry.marketplaces.find((m) => m.name === name);
|
|
386
|
+
if (!marketplace) {
|
|
387
|
+
throw new Error(`Marketplace ${name} not found`);
|
|
388
|
+
}
|
|
389
|
+
marketplace.autoUpdate = enabled;
|
|
390
|
+
await this.saveKnownMarketplaces(registry);
|
|
391
|
+
});
|
|
333
392
|
}
|
|
334
393
|
/**
|
|
335
394
|
* Installs a plugin from a marketplace
|
|
336
395
|
*/
|
|
337
396
|
async installPlugin(pluginAtMarketplace, projectPath) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const marketplaces = await this.listMarketplaces();
|
|
343
|
-
const marketplace = marketplaces.find((m) => m.name === marketplaceName);
|
|
344
|
-
if (!marketplace) {
|
|
345
|
-
throw new Error(`Marketplace ${marketplaceName} not found`);
|
|
346
|
-
}
|
|
347
|
-
const marketplacePath = this.getMarketplacePath(marketplace);
|
|
348
|
-
const manifest = await this.loadMarketplaceManifest(marketplacePath);
|
|
349
|
-
const pluginEntry = manifest.plugins.find((p) => p.name === pluginName);
|
|
350
|
-
if (!pluginEntry) {
|
|
351
|
-
throw new Error(`Plugin ${pluginName} not found in marketplace ${marketplaceName}`);
|
|
352
|
-
}
|
|
353
|
-
const isGitSource = pluginEntry.source.startsWith("http://") ||
|
|
354
|
-
pluginEntry.source.startsWith("https://") ||
|
|
355
|
-
pluginEntry.source.startsWith("git@") ||
|
|
356
|
-
pluginEntry.source.startsWith("ssh://");
|
|
357
|
-
let pluginSrcPath;
|
|
358
|
-
let tempCloneDir;
|
|
359
|
-
if (isGitSource) {
|
|
360
|
-
tempCloneDir = path.join(this.tmpDir, `clone-${Date.now()}`);
|
|
361
|
-
let url = pluginEntry.source;
|
|
362
|
-
let ref;
|
|
363
|
-
if (url.includes("#")) {
|
|
364
|
-
[url, ref] = url.split("#");
|
|
397
|
+
return this.withLock(async () => {
|
|
398
|
+
const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
|
|
399
|
+
if (!pluginName || !marketplaceName) {
|
|
400
|
+
throw new Error("Invalid plugin format. Use name@marketplace");
|
|
365
401
|
}
|
|
366
|
-
await this.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
pluginSrcPath = path.resolve(marketplacePath, pluginEntry.source);
|
|
371
|
-
}
|
|
372
|
-
const pluginManifestPath = path.join(pluginSrcPath, ".wave-plugin", "plugin.json");
|
|
373
|
-
if (!existsSync(pluginManifestPath)) {
|
|
374
|
-
if (tempCloneDir) {
|
|
375
|
-
await fs.rm(tempCloneDir, { recursive: true, force: true });
|
|
402
|
+
const marketplaces = await this.listMarketplaces();
|
|
403
|
+
const marketplace = marketplaces.find((m) => m.name === marketplaceName);
|
|
404
|
+
if (!marketplace) {
|
|
405
|
+
throw new Error(`Marketplace ${marketplaceName} not found`);
|
|
376
406
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
// Atomic installation
|
|
383
|
-
const tmpPluginDir = path.join(this.tmpDir, `${pluginName}-${Date.now()}`);
|
|
384
|
-
try {
|
|
385
|
-
if (isGitSource) {
|
|
386
|
-
await fs.rename(pluginSrcPath, tmpPluginDir);
|
|
387
|
-
tempCloneDir = undefined; // Already moved
|
|
407
|
+
const marketplacePath = this.getMarketplacePath(marketplace);
|
|
408
|
+
const manifest = await this.loadMarketplaceManifest(marketplacePath);
|
|
409
|
+
const pluginEntry = manifest.plugins.find((p) => p.name === pluginName);
|
|
410
|
+
if (!pluginEntry) {
|
|
411
|
+
throw new Error(`Plugin ${pluginName} not found in marketplace ${marketplaceName}`);
|
|
388
412
|
}
|
|
389
|
-
|
|
390
|
-
|
|
413
|
+
const isGitSource = pluginEntry.source.startsWith("http://") ||
|
|
414
|
+
pluginEntry.source.startsWith("https://") ||
|
|
415
|
+
pluginEntry.source.startsWith("git@") ||
|
|
416
|
+
pluginEntry.source.startsWith("ssh://");
|
|
417
|
+
let pluginSrcPath;
|
|
418
|
+
let tempCloneDir;
|
|
419
|
+
try {
|
|
420
|
+
if (isGitSource) {
|
|
421
|
+
tempCloneDir = path.join(this.tmpDir, `clone-${Date.now()}`);
|
|
422
|
+
let url = pluginEntry.source;
|
|
423
|
+
let ref;
|
|
424
|
+
if (url.includes("#")) {
|
|
425
|
+
[url, ref] = url.split("#");
|
|
426
|
+
}
|
|
427
|
+
await this.gitService.clone(url, tempCloneDir, ref);
|
|
428
|
+
pluginSrcPath = tempCloneDir;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
pluginSrcPath = path.resolve(marketplacePath, pluginEntry.source);
|
|
432
|
+
}
|
|
433
|
+
const pluginManifestPath = path.join(pluginSrcPath, ".wave-plugin", "plugin.json");
|
|
434
|
+
if (!existsSync(pluginManifestPath)) {
|
|
435
|
+
throw new Error(`Plugin manifest not found at ${pluginManifestPath}`);
|
|
436
|
+
}
|
|
437
|
+
const pluginManifestContent = await fs.readFile(pluginManifestPath, "utf-8");
|
|
438
|
+
const pluginManifest = JSON.parse(pluginManifestContent);
|
|
439
|
+
const version = pluginManifest.version || "1.0.0";
|
|
440
|
+
// Atomic installation
|
|
441
|
+
const tmpPluginDir = path.join(this.tmpDir, `${pluginName}-${Date.now()}`);
|
|
442
|
+
try {
|
|
443
|
+
if (isGitSource) {
|
|
444
|
+
await fs.rename(pluginSrcPath, tmpPluginDir);
|
|
445
|
+
tempCloneDir = undefined; // Already moved
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
await fs.cp(pluginSrcPath, tmpPluginDir, { recursive: true });
|
|
449
|
+
}
|
|
450
|
+
const cachePath = path.join(this.cacheDir, marketplaceName, pluginName, version);
|
|
451
|
+
if (existsSync(cachePath)) {
|
|
452
|
+
await fs.rm(cachePath, { recursive: true, force: true });
|
|
453
|
+
}
|
|
454
|
+
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
|
455
|
+
await fs.rename(tmpPluginDir, cachePath);
|
|
456
|
+
const installedRegistry = await this.getInstalledPlugins();
|
|
457
|
+
const existingIndex = installedRegistry.plugins.findIndex((p) => p.name === pluginName &&
|
|
458
|
+
p.marketplace === marketplaceName &&
|
|
459
|
+
p.projectPath === projectPath);
|
|
460
|
+
const installedPlugin = {
|
|
461
|
+
name: pluginName,
|
|
462
|
+
marketplace: marketplaceName,
|
|
463
|
+
version,
|
|
464
|
+
cachePath,
|
|
465
|
+
projectPath,
|
|
466
|
+
};
|
|
467
|
+
if (existingIndex >= 0) {
|
|
468
|
+
installedRegistry.plugins[existingIndex] = installedPlugin;
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
installedRegistry.plugins.push(installedPlugin);
|
|
472
|
+
}
|
|
473
|
+
await this.saveInstalledPlugins(installedRegistry);
|
|
474
|
+
return installedPlugin;
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
// Cleanup tmp dir if it exists
|
|
478
|
+
if (existsSync(tmpPluginDir)) {
|
|
479
|
+
await fs.rm(tmpPluginDir, { recursive: true, force: true });
|
|
480
|
+
}
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
391
483
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
484
|
+
catch (error) {
|
|
485
|
+
// Cleanup temp clone dir if it exists
|
|
486
|
+
if (tempCloneDir && existsSync(tempCloneDir)) {
|
|
487
|
+
await fs.rm(tempCloneDir, { recursive: true, force: true });
|
|
488
|
+
}
|
|
489
|
+
throw new Error(`Failed to install plugin ${pluginName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Uninstalls a plugin
|
|
495
|
+
*/
|
|
496
|
+
async uninstallPlugin(pluginAtMarketplace, projectPath) {
|
|
497
|
+
return this.withLock(async () => {
|
|
498
|
+
const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
|
|
499
|
+
if (!pluginName || !marketplaceName) {
|
|
500
|
+
throw new Error("Invalid plugin format. Use name@marketplace");
|
|
395
501
|
}
|
|
396
|
-
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
|
397
|
-
await fs.rename(tmpPluginDir, cachePath);
|
|
398
502
|
const installedRegistry = await this.getInstalledPlugins();
|
|
399
|
-
const
|
|
503
|
+
const pluginIndex = installedRegistry.plugins.findIndex((p) => p.name === pluginName &&
|
|
400
504
|
p.marketplace === marketplaceName &&
|
|
401
505
|
p.projectPath === projectPath);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
marketplace: marketplaceName,
|
|
405
|
-
version,
|
|
406
|
-
cachePath,
|
|
407
|
-
projectPath,
|
|
408
|
-
};
|
|
409
|
-
if (existingIndex >= 0) {
|
|
410
|
-
installedRegistry.plugins[existingIndex] = installedPlugin;
|
|
411
|
-
}
|
|
412
|
-
else {
|
|
413
|
-
installedRegistry.plugins.push(installedPlugin);
|
|
506
|
+
if (pluginIndex === -1) {
|
|
507
|
+
throw new Error(`Plugin ${pluginName}@${marketplaceName} is not installed${projectPath ? ` for project ${projectPath}` : ""}`);
|
|
414
508
|
}
|
|
509
|
+
const pluginToRemove = installedRegistry.plugins[pluginIndex];
|
|
510
|
+
// Remove from registry first
|
|
511
|
+
installedRegistry.plugins.splice(pluginIndex, 1);
|
|
415
512
|
await this.saveInstalledPlugins(installedRegistry);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
await fs.rm(tmpPluginDir, { recursive: true, force: true });
|
|
513
|
+
// Check if any other project is still using this same cache path
|
|
514
|
+
const isStillReferenced = installedRegistry.plugins.some((p) => p.cachePath === pluginToRemove.cachePath);
|
|
515
|
+
// Only remove cached files if no other references exist
|
|
516
|
+
if (!isStillReferenced && existsSync(pluginToRemove.cachePath)) {
|
|
517
|
+
await fs.rm(pluginToRemove.cachePath, { recursive: true, force: true });
|
|
422
518
|
}
|
|
423
|
-
|
|
424
|
-
await fs.rm(tempCloneDir, { recursive: true, force: true });
|
|
425
|
-
}
|
|
426
|
-
throw new Error(`Failed to install plugin ${pluginName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Uninstalls a plugin
|
|
431
|
-
*/
|
|
432
|
-
async uninstallPlugin(pluginAtMarketplace, projectPath) {
|
|
433
|
-
const [pluginName, marketplaceName] = pluginAtMarketplace.split("@");
|
|
434
|
-
if (!pluginName || !marketplaceName) {
|
|
435
|
-
throw new Error("Invalid plugin format. Use name@marketplace");
|
|
436
|
-
}
|
|
437
|
-
const installedRegistry = await this.getInstalledPlugins();
|
|
438
|
-
const pluginIndex = installedRegistry.plugins.findIndex((p) => p.name === pluginName &&
|
|
439
|
-
p.marketplace === marketplaceName &&
|
|
440
|
-
p.projectPath === projectPath);
|
|
441
|
-
if (pluginIndex === -1) {
|
|
442
|
-
throw new Error(`Plugin ${pluginName}@${marketplaceName} is not installed${projectPath ? ` for project ${projectPath}` : ""}`);
|
|
443
|
-
}
|
|
444
|
-
const pluginToRemove = installedRegistry.plugins[pluginIndex];
|
|
445
|
-
// Remove from registry first
|
|
446
|
-
installedRegistry.plugins.splice(pluginIndex, 1);
|
|
447
|
-
await this.saveInstalledPlugins(installedRegistry);
|
|
448
|
-
// Check if any other project is still using this same cache path
|
|
449
|
-
const isStillReferenced = installedRegistry.plugins.some((p) => p.cachePath === pluginToRemove.cachePath);
|
|
450
|
-
// Only remove cached files if no other references exist
|
|
451
|
-
if (!isStillReferenced && existsSync(pluginToRemove.cachePath)) {
|
|
452
|
-
await fs.rm(pluginToRemove.cachePath, { recursive: true, force: true });
|
|
453
|
-
}
|
|
519
|
+
});
|
|
454
520
|
}
|
|
455
521
|
/**
|
|
456
522
|
* Updates a plugin (uninstall followed by install)
|
|
457
523
|
*/
|
|
458
524
|
async updatePlugin(pluginAtMarketplace) {
|
|
459
|
-
|
|
460
|
-
|
|
525
|
+
return this.withLock(async () => {
|
|
526
|
+
await this.uninstallPlugin(pluginAtMarketplace);
|
|
527
|
+
return this.installPlugin(pluginAtMarketplace);
|
|
528
|
+
});
|
|
461
529
|
}
|
|
462
530
|
}
|
|
531
|
+
MarketplaceService.isLockedInProcess = false;
|
|
463
532
|
MarketplaceService.BUILTIN_MARKETPLACE = {
|
|
464
533
|
name: "wave-plugins-official",
|
|
465
534
|
source: {
|