@checkstack/backend 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +333 -0
- package/drizzle/0001_slim_mordo.sql +34 -0
- package/drizzle/meta/0001_snapshot.json +444 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +18 -13
- package/src/index.ts +276 -17
- package/src/plugin-deregistration.test.ts +137 -0
- package/src/plugin-manager/api-router.ts +35 -11
- package/src/plugin-manager/plugin-loader.ts +73 -0
- package/src/plugin-manager.ts +295 -105
- package/src/schema.ts +79 -1
- package/src/services/cache-manager.test.ts +172 -0
- package/src/services/cache-manager.ts +67 -14
- package/src/services/compatibility-checker.test.ts +146 -0
- package/src/services/compatibility-checker.ts +137 -0
- package/src/services/dev-auth.test.ts +87 -0
- package/src/services/dev-auth.ts +56 -0
- package/src/services/event-bus.test.ts +52 -0
- package/src/services/event-bus.ts +27 -1
- package/src/services/plugin-artifact-store.ts +131 -0
- package/src/services/plugin-bundle-resolver.ts +76 -0
- package/src/services/plugin-event-recorder.ts +87 -0
- package/src/services/plugin-installers/catalog-installer.ts +33 -0
- package/src/services/plugin-installers/github-installer.ts +207 -0
- package/src/services/plugin-installers/install-from-tarball.ts +69 -0
- package/src/services/plugin-installers/installer-registry.ts +51 -0
- package/src/services/plugin-installers/npm-installer.ts +156 -0
- package/src/services/plugin-installers/plugin-install-error.ts +37 -0
- package/src/services/plugin-installers/tarball-installer.ts +80 -0
- package/src/services/plugin-installers/tarball-utils.test.ts +200 -0
- package/src/services/plugin-installers/tarball-utils.ts +172 -0
- package/src/services/plugin-manager-orchestrator.ts +522 -0
- package/src/services/plugin-manager-router.ts +219 -0
- package/src/services/queue-manager.ts +77 -2
- package/src/services/queue-proxy.ts +7 -0
- package/src/utils/plugin-discovery.test.ts +6 -0
- package/src/utils/plugin-discovery.ts +6 -1
- package/tsconfig.json +3 -0
- package/src/plugin-lifecycle.test.ts +0 -276
- package/src/plugin-manager/plugin-admin-router.ts +0 -89
- package/src/services/plugin-installer.test.ts +0 -90
- package/src/services/plugin-installer.ts +0 -70
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { eq, and, sql, inArray } from "drizzle-orm";
|
|
4
|
+
import {
|
|
5
|
+
coreServices,
|
|
6
|
+
type SafeDatabase,
|
|
7
|
+
type PluginSource,
|
|
8
|
+
} from "@checkstack/backend-api";
|
|
9
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
10
|
+
import {
|
|
11
|
+
installPreviewSchema,
|
|
12
|
+
uninstallPreviewSchema,
|
|
13
|
+
type InstallPreview,
|
|
14
|
+
type UninstallPreview,
|
|
15
|
+
} from "@checkstack/pluginmanager-common";
|
|
16
|
+
import type { PluginManager } from "../plugin-manager";
|
|
17
|
+
import type { ServiceRegistry } from "./service-registry";
|
|
18
|
+
import { plugins, pluginConfigs } from "../schema";
|
|
19
|
+
import { resolveBundle } from "./plugin-bundle-resolver";
|
|
20
|
+
import {
|
|
21
|
+
loadCheckstackPackageVersions,
|
|
22
|
+
checkCompatibility,
|
|
23
|
+
} from "./compatibility-checker";
|
|
24
|
+
import type { PluginEventRecorder } from "./plugin-event-recorder";
|
|
25
|
+
import { PluginInstallError } from "./plugin-installers/plugin-install-error";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Originator-side install/uninstall orchestration.
|
|
29
|
+
*
|
|
30
|
+
* Lives next to the router because both flows are tightly coupled to the
|
|
31
|
+
* `pluginManager` instance, the artifact store, and `db`. The router is a
|
|
32
|
+
* thin oRPC wrapper over these functions.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export async function previewInstallOriginator({
|
|
36
|
+
source,
|
|
37
|
+
registry,
|
|
38
|
+
workspaceRoot,
|
|
39
|
+
runtimeDir,
|
|
40
|
+
}: {
|
|
41
|
+
source: PluginSource;
|
|
42
|
+
registry: ServiceRegistry;
|
|
43
|
+
workspaceRoot: string;
|
|
44
|
+
runtimeDir: string;
|
|
45
|
+
}): Promise<InstallPreview> {
|
|
46
|
+
const installerRegistry = await registry.get(
|
|
47
|
+
coreServices.pluginInstallerRegistry,
|
|
48
|
+
{ pluginId: "core" },
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const { primary, packages } = await resolveBundle({
|
|
52
|
+
source,
|
|
53
|
+
installerRegistry,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const loadedVersions = loadCheckstackPackageVersions({
|
|
57
|
+
workspaceRoot,
|
|
58
|
+
runtimeDir,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const compatibilityIssues = checkCompatibility({
|
|
62
|
+
packages: packages.map((p) => p.packageJson),
|
|
63
|
+
loadedVersions,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const totalSizeBytes = packages.reduce(
|
|
67
|
+
(sum, p) => sum + p.tarball.byteLength,
|
|
68
|
+
0,
|
|
69
|
+
);
|
|
70
|
+
const hasInstallScripts = packages.some(
|
|
71
|
+
(p) => p.packageJson.checkstack.allowInstallScripts === true,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return installPreviewSchema.parse({
|
|
75
|
+
primary: primary.packageJson,
|
|
76
|
+
packages: packages.map((p) => p.packageJson),
|
|
77
|
+
compatibilityIssues,
|
|
78
|
+
totalSizeBytes,
|
|
79
|
+
hasInstallScripts,
|
|
80
|
+
} satisfies InstallPreview);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function installOriginator({
|
|
84
|
+
source,
|
|
85
|
+
pluginManager,
|
|
86
|
+
db,
|
|
87
|
+
registry,
|
|
88
|
+
workspaceRoot,
|
|
89
|
+
runtimeDir,
|
|
90
|
+
eventRecorder,
|
|
91
|
+
userId,
|
|
92
|
+
}: {
|
|
93
|
+
source: PluginSource;
|
|
94
|
+
pluginManager: PluginManager;
|
|
95
|
+
db: SafeDatabase<Record<string, unknown>>;
|
|
96
|
+
registry: ServiceRegistry;
|
|
97
|
+
workspaceRoot: string;
|
|
98
|
+
runtimeDir: string;
|
|
99
|
+
eventRecorder: PluginEventRecorder;
|
|
100
|
+
userId?: string;
|
|
101
|
+
}): Promise<{
|
|
102
|
+
bundleId: string | null;
|
|
103
|
+
installedPackages: string[];
|
|
104
|
+
}> {
|
|
105
|
+
const installerRegistry = await registry.get(
|
|
106
|
+
coreServices.pluginInstallerRegistry,
|
|
107
|
+
{ pluginId: "core" },
|
|
108
|
+
);
|
|
109
|
+
const artifactStore = await registry.get(coreServices.pluginArtifactStore, {
|
|
110
|
+
pluginId: "core",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 1. Validate (re-fetch + recheck — preview is non-binding)
|
|
114
|
+
await eventRecorder.record({
|
|
115
|
+
action: "install",
|
|
116
|
+
phase: "validate",
|
|
117
|
+
status: "started",
|
|
118
|
+
source,
|
|
119
|
+
userId,
|
|
120
|
+
});
|
|
121
|
+
const { primary, packages } = await resolveBundle({
|
|
122
|
+
source,
|
|
123
|
+
installerRegistry,
|
|
124
|
+
});
|
|
125
|
+
const loadedVersions = loadCheckstackPackageVersions({
|
|
126
|
+
workspaceRoot,
|
|
127
|
+
runtimeDir,
|
|
128
|
+
});
|
|
129
|
+
const issues = checkCompatibility({
|
|
130
|
+
packages: packages.map((p) => p.packageJson),
|
|
131
|
+
loadedVersions,
|
|
132
|
+
});
|
|
133
|
+
if (issues.length > 0) {
|
|
134
|
+
const message = issues.map((i) => i.message).join("\n");
|
|
135
|
+
await eventRecorder.record({
|
|
136
|
+
pluginName: primary.packageJson.name,
|
|
137
|
+
action: "install",
|
|
138
|
+
phase: "validate",
|
|
139
|
+
status: "failed",
|
|
140
|
+
source,
|
|
141
|
+
error: message,
|
|
142
|
+
userId,
|
|
143
|
+
});
|
|
144
|
+
throw new PluginInstallError(
|
|
145
|
+
"COMPATIBILITY_FAILED",
|
|
146
|
+
`This plugin isn't compatible with the platform:\n${message}`,
|
|
147
|
+
{ issues },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
await eventRecorder.record({
|
|
151
|
+
pluginName: primary.packageJson.name,
|
|
152
|
+
action: "install",
|
|
153
|
+
phase: "validate",
|
|
154
|
+
status: "succeeded",
|
|
155
|
+
source,
|
|
156
|
+
userId,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// 2. Persist artifacts + plugins rows. One bundle = one bundleId.
|
|
160
|
+
// Single-package installs use `null` (drizzle writes a real NULL column),
|
|
161
|
+
// not `undefined` (which would skip the column on insert).
|
|
162
|
+
const bundleId: string | null =
|
|
163
|
+
packages.length > 1 ? randomUUID() : null;
|
|
164
|
+
await eventRecorder.record({
|
|
165
|
+
pluginName: primary.packageJson.name,
|
|
166
|
+
bundleId,
|
|
167
|
+
action: "install",
|
|
168
|
+
phase: "persist",
|
|
169
|
+
status: "started",
|
|
170
|
+
source,
|
|
171
|
+
userId,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
for (const pkg of packages) {
|
|
176
|
+
await artifactStore.store({
|
|
177
|
+
pluginName: pkg.packageJson.name,
|
|
178
|
+
version: pkg.packageJson.version,
|
|
179
|
+
bundleId,
|
|
180
|
+
tarball: pkg.tarball,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Insert / update plugins row. We do NOT install into node_modules
|
|
184
|
+
// here on the originator — that happens uniformly on every instance
|
|
185
|
+
// (originator included) when the install broadcast is handled.
|
|
186
|
+
const isPrimary = pkg.packageJson.name === primary.packageJson.name;
|
|
187
|
+
const expectedPath = path.join(
|
|
188
|
+
runtimeDir,
|
|
189
|
+
"node_modules",
|
|
190
|
+
pkg.packageJson.name,
|
|
191
|
+
);
|
|
192
|
+
await db
|
|
193
|
+
.insert(plugins)
|
|
194
|
+
.values({
|
|
195
|
+
name: pkg.packageJson.name,
|
|
196
|
+
path: expectedPath,
|
|
197
|
+
enabled: true,
|
|
198
|
+
isUninstallable: true,
|
|
199
|
+
type: pkg.packageJson.checkstack.type,
|
|
200
|
+
version: pkg.packageJson.version,
|
|
201
|
+
metadata: pkg.packageJson,
|
|
202
|
+
source,
|
|
203
|
+
bundleId,
|
|
204
|
+
isPrimary,
|
|
205
|
+
})
|
|
206
|
+
.onConflictDoUpdate({
|
|
207
|
+
target: [plugins.name],
|
|
208
|
+
set: {
|
|
209
|
+
path: expectedPath,
|
|
210
|
+
enabled: true,
|
|
211
|
+
isUninstallable: true,
|
|
212
|
+
type: pkg.packageJson.checkstack.type,
|
|
213
|
+
version: pkg.packageJson.version,
|
|
214
|
+
metadata: pkg.packageJson,
|
|
215
|
+
source,
|
|
216
|
+
bundleId,
|
|
217
|
+
isPrimary,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
await eventRecorder.record({
|
|
222
|
+
pluginName: primary.packageJson.name,
|
|
223
|
+
bundleId,
|
|
224
|
+
action: "install",
|
|
225
|
+
phase: "persist",
|
|
226
|
+
status: "succeeded",
|
|
227
|
+
source,
|
|
228
|
+
userId,
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
await eventRecorder.record({
|
|
232
|
+
pluginName: primary.packageJson.name,
|
|
233
|
+
bundleId,
|
|
234
|
+
action: "install",
|
|
235
|
+
phase: "persist",
|
|
236
|
+
status: "failed",
|
|
237
|
+
source,
|
|
238
|
+
error: extractErrorMessage(error),
|
|
239
|
+
userId,
|
|
240
|
+
});
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 3. Broadcast — every instance (incl. originator) hydrates from
|
|
245
|
+
// plugin_artifacts and loads the module. Backend packages first so
|
|
246
|
+
// frontend siblings have access to backend services on init.
|
|
247
|
+
const sortedPackageNames = packages
|
|
248
|
+
.toSorted(
|
|
249
|
+
(a, b) =>
|
|
250
|
+
orderForType(a.packageJson.checkstack.type) -
|
|
251
|
+
orderForType(b.packageJson.checkstack.type),
|
|
252
|
+
)
|
|
253
|
+
.map((p) => p.packageJson.name);
|
|
254
|
+
|
|
255
|
+
await eventRecorder.record({
|
|
256
|
+
pluginName: primary.packageJson.name,
|
|
257
|
+
bundleId,
|
|
258
|
+
action: "install",
|
|
259
|
+
phase: "broadcast",
|
|
260
|
+
status: "started",
|
|
261
|
+
source,
|
|
262
|
+
userId,
|
|
263
|
+
});
|
|
264
|
+
try {
|
|
265
|
+
await pluginManager.broadcastInstallation(sortedPackageNames);
|
|
266
|
+
await eventRecorder.record({
|
|
267
|
+
pluginName: primary.packageJson.name,
|
|
268
|
+
bundleId,
|
|
269
|
+
action: "install",
|
|
270
|
+
phase: "broadcast",
|
|
271
|
+
status: "succeeded",
|
|
272
|
+
source,
|
|
273
|
+
userId,
|
|
274
|
+
});
|
|
275
|
+
} catch (error) {
|
|
276
|
+
await eventRecorder.record({
|
|
277
|
+
pluginName: primary.packageJson.name,
|
|
278
|
+
bundleId,
|
|
279
|
+
action: "install",
|
|
280
|
+
phase: "broadcast",
|
|
281
|
+
status: "failed",
|
|
282
|
+
source,
|
|
283
|
+
error: extractErrorMessage(error),
|
|
284
|
+
userId,
|
|
285
|
+
});
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
bundleId,
|
|
291
|
+
installedPackages: sortedPackageNames,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function previewUninstallOriginator({
|
|
296
|
+
pluginName,
|
|
297
|
+
db,
|
|
298
|
+
}: {
|
|
299
|
+
pluginName: string;
|
|
300
|
+
db: SafeDatabase<Record<string, unknown>>;
|
|
301
|
+
}): Promise<UninstallPreview> {
|
|
302
|
+
const rows = await db
|
|
303
|
+
.select()
|
|
304
|
+
.from(plugins)
|
|
305
|
+
.where(eq(plugins.name, pluginName))
|
|
306
|
+
.limit(1);
|
|
307
|
+
if (rows.length === 0) {
|
|
308
|
+
throw new PluginInstallError(
|
|
309
|
+
"NOT_FOUND",
|
|
310
|
+
`Plugin '${pluginName}' is not installed.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
const target = rows[0];
|
|
314
|
+
if (!target.isUninstallable) {
|
|
315
|
+
throw new PluginInstallError(
|
|
316
|
+
"FORBIDDEN",
|
|
317
|
+
`Plugin '${pluginName}' is part of the platform core and cannot be uninstalled.`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const siblings: string[] = [];
|
|
322
|
+
if (target.bundleId) {
|
|
323
|
+
const sibRows = await db
|
|
324
|
+
.select({ name: plugins.name })
|
|
325
|
+
.from(plugins)
|
|
326
|
+
.where(eq(plugins.bundleId, target.bundleId));
|
|
327
|
+
for (const r of sibRows) siblings.push(r.name);
|
|
328
|
+
} else {
|
|
329
|
+
siblings.push(target.name);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Collect the set of plugin ids whose schemas would drop.
|
|
333
|
+
const schemasToDrop = siblings.map((s) => `plugin_${s}`);
|
|
334
|
+
|
|
335
|
+
// Plugin configs row count for siblings.
|
|
336
|
+
const allConfigsRows = await db
|
|
337
|
+
.select({ count: sql<string>`count(*)` })
|
|
338
|
+
.from(pluginConfigs)
|
|
339
|
+
.where(inArray(pluginConfigs.pluginId, siblings));
|
|
340
|
+
const pluginConfigCount = Number(allConfigsRows[0]?.count ?? 0);
|
|
341
|
+
|
|
342
|
+
// Dependents — other (loaded) runtime plugins whose @checkstack/* deps
|
|
343
|
+
// include any of the siblings.
|
|
344
|
+
const dependents: string[] = [];
|
|
345
|
+
const otherRows = await db
|
|
346
|
+
.select()
|
|
347
|
+
.from(plugins)
|
|
348
|
+
.where(and(eq(plugins.isUninstallable, true)));
|
|
349
|
+
for (const row of otherRows) {
|
|
350
|
+
if (siblings.includes(row.name)) continue;
|
|
351
|
+
const deps =
|
|
352
|
+
(row.metadata as { dependencies?: Record<string, string> })?.dependencies ??
|
|
353
|
+
{};
|
|
354
|
+
if (Object.keys(deps).some((d) => siblings.includes(d))) {
|
|
355
|
+
dependents.push(row.name);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return uninstallPreviewSchema.parse({
|
|
360
|
+
pluginName: target.name,
|
|
361
|
+
siblings,
|
|
362
|
+
dependents,
|
|
363
|
+
schemasToDrop,
|
|
364
|
+
pluginConfigCount,
|
|
365
|
+
} satisfies UninstallPreview);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export async function uninstallOriginator({
|
|
369
|
+
pluginName,
|
|
370
|
+
deleteSchema,
|
|
371
|
+
deleteConfigs,
|
|
372
|
+
cascade,
|
|
373
|
+
pluginManager,
|
|
374
|
+
db,
|
|
375
|
+
eventRecorder,
|
|
376
|
+
userId,
|
|
377
|
+
}: {
|
|
378
|
+
pluginName: string;
|
|
379
|
+
deleteSchema: boolean;
|
|
380
|
+
deleteConfigs: boolean;
|
|
381
|
+
cascade: boolean;
|
|
382
|
+
pluginManager: PluginManager;
|
|
383
|
+
db: SafeDatabase<Record<string, unknown>>;
|
|
384
|
+
eventRecorder: PluginEventRecorder;
|
|
385
|
+
userId?: string;
|
|
386
|
+
}): Promise<{ uninstalledPackages: string[] }> {
|
|
387
|
+
const target = await db
|
|
388
|
+
.select()
|
|
389
|
+
.from(plugins)
|
|
390
|
+
.where(eq(plugins.name, pluginName))
|
|
391
|
+
.limit(1)
|
|
392
|
+
.then((rows) => rows[0]);
|
|
393
|
+
if (!target) {
|
|
394
|
+
throw new PluginInstallError(
|
|
395
|
+
"NOT_FOUND",
|
|
396
|
+
`Plugin '${pluginName}' is not installed.`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
if (!target.isUninstallable) {
|
|
400
|
+
throw new PluginInstallError(
|
|
401
|
+
"FORBIDDEN",
|
|
402
|
+
`Plugin '${pluginName}' is part of the platform core and cannot be uninstalled.`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const ids: string[] = [];
|
|
407
|
+
if (target.bundleId) {
|
|
408
|
+
const sibRows = await db
|
|
409
|
+
.select({ name: plugins.name })
|
|
410
|
+
.from(plugins)
|
|
411
|
+
.where(eq(plugins.bundleId, target.bundleId));
|
|
412
|
+
for (const r of sibRows) ids.push(r.name);
|
|
413
|
+
} else {
|
|
414
|
+
ids.push(target.name);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (cascade) {
|
|
418
|
+
// Find all dependents recursively via metadata.dependencies.
|
|
419
|
+
const queue = [...ids];
|
|
420
|
+
const seen = new Set(ids);
|
|
421
|
+
while (queue.length > 0) {
|
|
422
|
+
const current = queue.shift()!;
|
|
423
|
+
const candidates = await db
|
|
424
|
+
.select()
|
|
425
|
+
.from(plugins)
|
|
426
|
+
.where(and(eq(plugins.isUninstallable, true)));
|
|
427
|
+
for (const row of candidates) {
|
|
428
|
+
if (seen.has(row.name)) continue;
|
|
429
|
+
const deps =
|
|
430
|
+
(row.metadata as { dependencies?: Record<string, string> })
|
|
431
|
+
?.dependencies ?? {};
|
|
432
|
+
if (Object.keys(deps).includes(current)) {
|
|
433
|
+
ids.push(row.name);
|
|
434
|
+
seen.add(row.name);
|
|
435
|
+
queue.push(row.name);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 1. Broadcast in-process unload to all instances (incl. originator).
|
|
442
|
+
await eventRecorder.record({
|
|
443
|
+
pluginName,
|
|
444
|
+
bundleId: target.bundleId ?? undefined,
|
|
445
|
+
action: "uninstall",
|
|
446
|
+
phase: "broadcast",
|
|
447
|
+
status: "started",
|
|
448
|
+
userId,
|
|
449
|
+
});
|
|
450
|
+
try {
|
|
451
|
+
await pluginManager.broadcastDeregistration(ids);
|
|
452
|
+
await eventRecorder.record({
|
|
453
|
+
pluginName,
|
|
454
|
+
bundleId: target.bundleId ?? undefined,
|
|
455
|
+
action: "uninstall",
|
|
456
|
+
phase: "broadcast",
|
|
457
|
+
status: "succeeded",
|
|
458
|
+
userId,
|
|
459
|
+
});
|
|
460
|
+
} catch (error) {
|
|
461
|
+
await eventRecorder.record({
|
|
462
|
+
pluginName,
|
|
463
|
+
bundleId: target.bundleId ?? undefined,
|
|
464
|
+
action: "uninstall",
|
|
465
|
+
phase: "broadcast",
|
|
466
|
+
status: "failed",
|
|
467
|
+
error: extractErrorMessage(error),
|
|
468
|
+
userId,
|
|
469
|
+
});
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 2. Originator-only destructive cleanup.
|
|
474
|
+
await eventRecorder.record({
|
|
475
|
+
pluginName,
|
|
476
|
+
bundleId: target.bundleId ?? undefined,
|
|
477
|
+
action: "uninstall",
|
|
478
|
+
phase: "destructive-cleanup",
|
|
479
|
+
status: "started",
|
|
480
|
+
userId,
|
|
481
|
+
});
|
|
482
|
+
try {
|
|
483
|
+
await pluginManager.deletePluginData({
|
|
484
|
+
pluginIds: ids,
|
|
485
|
+
bundleId: target.bundleId ?? undefined,
|
|
486
|
+
deleteSchema,
|
|
487
|
+
deleteConfigs,
|
|
488
|
+
});
|
|
489
|
+
await eventRecorder.record({
|
|
490
|
+
pluginName,
|
|
491
|
+
bundleId: target.bundleId ?? undefined,
|
|
492
|
+
action: "uninstall",
|
|
493
|
+
phase: "destructive-cleanup",
|
|
494
|
+
status: "succeeded",
|
|
495
|
+
userId,
|
|
496
|
+
});
|
|
497
|
+
} catch (error) {
|
|
498
|
+
await eventRecorder.record({
|
|
499
|
+
pluginName,
|
|
500
|
+
bundleId: target.bundleId ?? undefined,
|
|
501
|
+
action: "uninstall",
|
|
502
|
+
phase: "destructive-cleanup",
|
|
503
|
+
status: "failed",
|
|
504
|
+
error: extractErrorMessage(error),
|
|
505
|
+
userId,
|
|
506
|
+
});
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return { uninstalledPackages: ids };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function orderForType(t: "backend" | "frontend" | "common"): number {
|
|
514
|
+
switch (t) {
|
|
515
|
+
case "common": { return 0;
|
|
516
|
+
}
|
|
517
|
+
case "backend": { return 1;
|
|
518
|
+
}
|
|
519
|
+
case "frontend": { return 2;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|