@capybara-hq/kernel 0.1.0-rc.10 → 0.1.0-rc.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/admin.schemas.d.ts +3 -0
- package/dist/catalog/catalog-content.d.ts +33 -1
- package/dist/catalog/catalog-content.js +65 -2
- package/dist/catalog/catalog-items.js +1 -0
- package/dist/catalog/catalog-scope.service.d.ts +29 -3
- package/dist/catalog/catalog-scope.service.js +82 -10
- package/dist/catalog/catalog.service.d.ts +7 -5
- package/dist/catalog/catalog.service.js +26 -5
- package/dist/catalog/claude/translate.js +3 -0
- package/dist/catalog/component-scope.schemas.d.ts +4 -0
- package/dist/catalog/component-scope.service.d.ts +48 -9
- package/dist/catalog/component-scope.service.js +152 -27
- package/dist/catalog/component-state.d.ts +12 -3
- package/dist/catalog/component-state.js +21 -3
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-catalog.service.js +3 -0
- package/dist/catalog/owned-items.js +7 -0
- package/dist/catalog/platform-components.d.ts +23 -3
- package/dist/catalog/platform-components.js +118 -9
- package/dist/catalog/platform-migration.js +2 -1
- package/dist/catalog/plugin-import.d.ts +24 -4
- package/dist/catalog/plugin-import.js +37 -6
- package/dist/catalog/plugin-install.d.ts +20 -1
- package/dist/catalog/plugin-install.js +71 -1
- package/dist/catalog/plugin-package.d.ts +41 -4
- package/dist/catalog/plugin-package.js +127 -7
- package/dist/catalog/plugin-upgrade.d.ts +13 -2
- package/dist/catalog/plugin-upgrade.js +124 -6
- package/dist/catalog/unshare.js +1 -0
- package/dist/db/kernel-schema.d.ts +41 -4
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +25 -0
- package/dist/http/api.module.js +7 -0
- package/dist/identity/accounts.d.ts +11 -4
- package/dist/identity/accounts.js +23 -10
- package/dist/identity/me.controller.d.ts +10 -10
- package/dist/identity/me.controller.js +8 -6
- package/dist/identity/me.schemas.d.ts +13 -3
- package/dist/identity/me.schemas.js +10 -2
- package/dist/identity/settings.d.ts +27 -0
- package/dist/identity/settings.js +64 -0
- package/dist/orgs/settings.service.d.ts +31 -0
- package/dist/orgs/settings.service.js +31 -1
- package/dist/platform/exclusions.js +16 -0
- package/dist/platform/operation.js +6 -2
- package/dist/platform/ops/admin.ops.d.ts +3 -1
- package/dist/platform/ops/admin.ops.js +25 -7
- package/dist/platform/ops/components.ops.js +1 -0
- package/dist/platform/ops/org-settings.ops.d.ts +3 -1
- package/dist/platform/ops/org-settings.ops.js +33 -8
- package/dist/platform/ops/orgs.ops.js +1 -0
- package/dist/platform/ops/platform.ops.d.ts +3 -1
- package/dist/platform/ops/platform.ops.js +43 -16
- package/dist/platform/ops/themes.ops.d.ts +14 -0
- package/dist/platform/ops/themes.ops.js +47 -0
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +5 -0
- package/dist/platform/route-schemas.js +3 -0
- package/dist/themes/render-css.d.ts +12 -0
- package/dist/themes/render-css.js +132 -0
- package/dist/themes/theme-delivery.service.d.ts +34 -0
- package/dist/themes/theme-delivery.service.js +111 -0
- package/dist/themes/theme-state.d.ts +95 -0
- package/dist/themes/theme-state.js +203 -0
- package/dist/themes/theme-usage.d.ts +61 -0
- package/dist/themes/theme-usage.js +98 -0
- package/dist/themes/themes-public.controller.d.ts +28 -0
- package/dist/themes/themes-public.controller.js +92 -0
- package/dist/themes/themes.controller.d.ts +13 -0
- package/dist/themes/themes.controller.js +38 -0
- package/dist/themes/themes.module.d.ts +3 -0
- package/dist/themes/themes.module.js +21 -0
- package/dist/themes/themes.schemas.d.ts +3 -0
- package/dist/themes/themes.schemas.js +2 -0
- package/dist/themes/themes.service.d.ts +50 -0
- package/dist/themes/themes.service.js +99 -0
- package/package.json +3 -3
- package/src/migrations/0102_themes.sql +64 -0
- package/src/migrations/0103_ui_theme_setting.sql +7 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import type { LocalizedName, ThemeScheme } from "@capybara-hq/plugin-sdk";
|
|
1
2
|
import { ApiError } from "../errors.js";
|
|
2
3
|
import { SuperAdminLock } from "../identity/super-admin.js";
|
|
3
4
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
4
5
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
5
6
|
import { OrgLock } from "../orgs/org-lock.js";
|
|
7
|
+
import { type ThemeSwatch } from "../themes/theme-state.js";
|
|
8
|
+
import { type ThemePreview } from "../themes/theme-usage.js";
|
|
6
9
|
import type { ActorOpts } from "./catalog.service.js";
|
|
7
10
|
import { ComponentPromotion } from "./component-promotion.js";
|
|
8
11
|
import { type ComponentKind } from "./component-state.js";
|
|
@@ -38,7 +41,14 @@ export interface ComponentScopePreview {
|
|
|
38
41
|
plugins: string[];
|
|
39
42
|
/** An agent template's: the owner of each clashing template, in `skills`' order. */
|
|
40
43
|
owners?: Owner[];
|
|
44
|
+
/** A theme's: the clashing themes on in the org (`skills` then empty). */
|
|
45
|
+
themes?: string[];
|
|
41
46
|
}[];
|
|
47
|
+
/**
|
|
48
|
+
* A theme the change takes away from some orgs (themes spec §6): the orgs among them whose
|
|
49
|
+
* default it is and how many people picked it. Absent when no theme is taken away.
|
|
50
|
+
*/
|
|
51
|
+
themes?: ThemePreview[];
|
|
42
52
|
scopeHash: string;
|
|
43
53
|
}
|
|
44
54
|
/** A component's own scope (`orgs` only with `named`), a disable, or following its plugin again. */
|
|
@@ -91,6 +101,11 @@ export interface ComponentView {
|
|
|
91
101
|
* other scopes, builtin plugins and orgs' own rows.
|
|
92
102
|
*/
|
|
93
103
|
offCount: number;
|
|
104
|
+
/** A theme: its name and swatches from its current version (absent for other kinds). */
|
|
105
|
+
theme?: {
|
|
106
|
+
name: LocalizedName;
|
|
107
|
+
swatches: Partial<Record<ThemeScheme, ThemeSwatch>>;
|
|
108
|
+
};
|
|
94
109
|
}
|
|
95
110
|
/** One version of a platform component (`GET admin/components/:kind/:id/versions`). */
|
|
96
111
|
export interface ComponentVersionView {
|
|
@@ -111,7 +126,10 @@ export interface OrgComponentView {
|
|
|
111
126
|
/** The org's switch (null: never switched). */
|
|
112
127
|
switch: boolean | null;
|
|
113
128
|
on: boolean;
|
|
114
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Of a builtin plugin, or the platform default theme (themes spec §7): it cannot be switched off
|
|
131
|
+
* (an `all` one can, layering spec §4).
|
|
132
|
+
*/
|
|
115
133
|
locked: boolean;
|
|
116
134
|
}
|
|
117
135
|
/**
|
|
@@ -157,6 +175,11 @@ export type ComponentSwitchResult = {
|
|
|
157
175
|
refs: string[];
|
|
158
176
|
}[];
|
|
159
177
|
connectorsKept: number;
|
|
178
|
+
/**
|
|
179
|
+
* A theme: whether it is the org's default and how many of its members picked it (themes
|
|
180
|
+
* spec §6). Absent for other kinds and for a theme nobody there chose.
|
|
181
|
+
*/
|
|
182
|
+
themes?: ThemePreview[];
|
|
160
183
|
};
|
|
161
184
|
confirm: string;
|
|
162
185
|
};
|
|
@@ -166,6 +189,8 @@ type AuditOpts = ActorOpts & {
|
|
|
166
189
|
};
|
|
167
190
|
/**
|
|
168
191
|
* 409 `plugin.skill_conflict` listing the orgs whose own skills use a component's name (rule 10);
|
|
192
|
+
* for a theme 409 `theme.id_taken` listing the orgs where another theme with its id is on (details
|
|
193
|
+
* `[{ theme, orgId, themes, plugins }]`);
|
|
169
194
|
* for an agent template 409 `agent_template.name_taken` listing the orgs that already use its
|
|
170
195
|
* roster type (ruling P4-13; details `[{ orgId, type, templateId, owner }]`, one per clashing
|
|
171
196
|
* template, `owner` as save-as names it).
|
|
@@ -181,10 +206,11 @@ export declare function nameConflictError(conflicts: ComponentScopePreview["conf
|
|
|
181
206
|
* An org admin switches one component on or off in the org (only that component, rule 5); one of a
|
|
182
207
|
* builtin plugin -> 409 `plugin.scope_locked` (an `all` one may go off, layering spec §4); switching
|
|
183
208
|
* off something in use is previewed with its agents first.
|
|
184
|
-
* Name clashes are checked for skills
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
209
|
+
* Name clashes are checked for skills, agent templates by their roster type, and themes by their
|
|
210
|
+
* id among the themes on in an org (themes spec §6). A change that would take the platform default
|
|
211
|
+
* theme away from an org is refused (409 `theme.platform_default`, themes spec §7). Components are
|
|
212
|
+
* resolved at read time (skills/usable.ts, agents/templates.ts), so nothing is enqueued.
|
|
213
|
+
* Stateless, shared by every replica.
|
|
188
214
|
*/
|
|
189
215
|
export declare class ComponentScopeService {
|
|
190
216
|
private readonly kdb;
|
|
@@ -203,7 +229,7 @@ export declare class ComponentScopeService {
|
|
|
203
229
|
/**
|
|
204
230
|
* What the admin routes act on (ruling P3-23): the platform component `id`, an org row the org
|
|
205
231
|
* promoted already (its platform row), or an org's own row not promoted yet. A connector
|
|
206
|
-
* template has no org rows; anything else -> 404 `component.not_found`.
|
|
232
|
+
* template or a theme has no org rows; anything else -> 404 `component.not_found`.
|
|
207
233
|
*/
|
|
208
234
|
private resolve;
|
|
209
235
|
/**
|
|
@@ -224,12 +250,13 @@ export declare class ComponentScopeService {
|
|
|
224
250
|
/**
|
|
225
251
|
* The org's copies of platform skills or agent templates linked without a pin (ruling B2), by
|
|
226
252
|
* platform id: they resolve to it, so agents that select them use it (none for connector
|
|
227
|
-
* templates).
|
|
253
|
+
* templates and themes).
|
|
228
254
|
*/
|
|
229
255
|
private linkedCopies;
|
|
230
256
|
/**
|
|
231
257
|
* The orgs' own skills (org scope) and agent templates, not linked to a platform component
|
|
232
|
-
* (ruling P2-11): listed read-only, on in their org only. Connector templates have none
|
|
258
|
+
* (ruling P2-11): listed read-only, on in their org only. Connector templates have none; an
|
|
259
|
+
* org's own theme is a platform_themes row with its org as owner, listed with the platform ones.
|
|
233
260
|
*/
|
|
234
261
|
private orgOwned;
|
|
235
262
|
/** The preview of a scope change, a disable or a return to following the plugin (nothing written). */
|
|
@@ -257,7 +284,8 @@ export declare class ComponentScopeService {
|
|
|
257
284
|
* The org admin's switch of one component (`PUT orgs/:orgId/components/:kind/:id/switch`): only
|
|
258
285
|
* this component (rule 5). A component not offered to the org -> 404. On: refused while the
|
|
259
286
|
* component or its plugin is disabled (`plugin.disabled`) or an own skill of the org has its name
|
|
260
|
-
* (`plugin.skill_conflict`, rule 10). Off:
|
|
287
|
+
* (`plugin.skill_conflict`, rule 10; a theme: `theme.id_taken`). Off: the platform default theme
|
|
288
|
+
* while it is on -> `theme.platform_default`; of a builtin plugin -> `plugin.scope_locked`; while it
|
|
261
289
|
* is on, without `confirm` the preview (agents that use it) and its hash are returned, with the
|
|
262
290
|
* current hash the switch is written (a changed preview -> `plugin.switch_stale`).
|
|
263
291
|
*/
|
|
@@ -314,6 +342,17 @@ export declare class ComponentScopeService {
|
|
|
314
342
|
* (an org row not promoted yet: `[owner]`), else read.
|
|
315
343
|
*/
|
|
316
344
|
private targetOf;
|
|
345
|
+
/**
|
|
346
|
+
* 409 `theme.platform_default` when theme `c` is the platform default and `t` would leave it
|
|
347
|
+
* narrower than `all` or gone (themes spec §7), in the caller's transaction, before any write.
|
|
348
|
+
*/
|
|
349
|
+
private assertKeepsPlatformDefault;
|
|
350
|
+
/**
|
|
351
|
+
* Theme `c`'s availability under `t`: gone when disabled (or its plugin is disabled or deleted
|
|
352
|
+
* while it follows it), else `all` when its effective scope (its own, or its plugin's; builtin:
|
|
353
|
+
* `all`) is `all`, else narrower.
|
|
354
|
+
*/
|
|
355
|
+
private themeAfter;
|
|
317
356
|
/** Live orgs (`only`: that one), sorted by id. */
|
|
318
357
|
private liveOrgs;
|
|
319
358
|
private on;
|
|
@@ -19,9 +19,11 @@ import { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
19
19
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
20
20
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
21
21
|
import { OrgLock } from "../orgs/org-lock.js";
|
|
22
|
+
import { themeDefinitions, themeSwatches } from "../themes/theme-state.js";
|
|
23
|
+
import { assertNotPlatformDefault, platformDefaultThemeId, themesPreview, } from "../themes/theme-usage.js";
|
|
22
24
|
import { assertOrgsExist } from "./catalog-items.js";
|
|
23
25
|
import { ComponentPromotion, notOwnedError, PROMOTION_ATTEMPTS, PromotionStale, promotionStaleError, } from "./component-promotion.js";
|
|
24
|
-
import { agentsUsing, COMPONENT_KINDS, componentLocked, componentOffered, componentOn, componentPluginOn, connectorsMadeFrom, listedIn, loadComponents, loadScopeState, pluginFactsOf, skillNameClashes, switchOf, } from "./component-state.js";
|
|
26
|
+
import { agentsUsing, COMPONENT_KINDS, componentLocked, componentOffered, componentOn, componentPluginOn, connectorsMadeFrom, listedIn, loadComponents, loadScopeState, pluginFactsOf, skillNameClashes, switchOf, themeIdClashes, } from "./component-state.js";
|
|
25
27
|
import { assertOwnerListed, assertShareable, assertShareableTarget, newlySharedWith, ownerOf, ownersOf, } from "./ownership.js";
|
|
26
28
|
const notFound = () => new ApiError("component.not_found", "Component not found");
|
|
27
29
|
const staleError = () => new ApiError("plugin.scope_stale", "What this change affects has changed since the preview; preview it again and confirm the new hash");
|
|
@@ -31,14 +33,27 @@ const ACTION = {
|
|
|
31
33
|
skill: "skills.manage",
|
|
32
34
|
agent_template: "agents.manage",
|
|
33
35
|
connector_template: "connectors.manage",
|
|
36
|
+
theme: "org.update",
|
|
34
37
|
};
|
|
35
38
|
/**
|
|
36
39
|
* 409 `plugin.skill_conflict` listing the orgs whose own skills use a component's name (rule 10);
|
|
40
|
+
* for a theme 409 `theme.id_taken` listing the orgs where another theme with its id is on (details
|
|
41
|
+
* `[{ theme, orgId, themes, plugins }]`);
|
|
37
42
|
* for an agent template 409 `agent_template.name_taken` listing the orgs that already use its
|
|
38
43
|
* roster type (ruling P4-13; details `[{ orgId, type, templateId, owner }]`, one per clashing
|
|
39
44
|
* template, `owner` as save-as names it).
|
|
40
45
|
*/
|
|
41
46
|
export function nameConflictError(conflicts, kind = "skill") {
|
|
47
|
+
if (kind === "theme") {
|
|
48
|
+
return new ApiError("theme.id_taken", conflicts
|
|
49
|
+
.map((c) => `A theme with id ${c.name} is already on in organization ${c.orgName} (plugin "${c.plugins.join('", "')}"); one theme id cannot be on there twice. Switch that theme off there first.`)
|
|
50
|
+
.join("\n"), conflicts.map((c) => ({
|
|
51
|
+
theme: c.name,
|
|
52
|
+
orgId: c.orgId,
|
|
53
|
+
themes: c.themes ?? [],
|
|
54
|
+
plugins: c.plugins,
|
|
55
|
+
})));
|
|
56
|
+
}
|
|
42
57
|
if (kind === "agent_template") {
|
|
43
58
|
// the save-as wording (ruling P4-9), naming each clashing template's owner
|
|
44
59
|
const whose = (o, orgId) => o === undefined || o.kind === "platform"
|
|
@@ -76,6 +91,8 @@ function previewHash(c, target, p) {
|
|
|
76
91
|
.sort((a, b) => a.orgId.localeCompare(b.orgId))
|
|
77
92
|
.map((a) => ({ ...a, agents: [...a.agents].sort((x, y) => x.id.localeCompare(y.id)) })),
|
|
78
93
|
conflicts: [...p.conflicts].sort((a, b) => a.orgId.localeCompare(b.orgId)),
|
|
94
|
+
// only when present: a preview without themes keeps the hash it had before themes
|
|
95
|
+
...(p.themes !== undefined && { themes: p.themes }),
|
|
79
96
|
}))
|
|
80
97
|
.digest("hex");
|
|
81
98
|
}
|
|
@@ -95,10 +112,11 @@ function switchHash(orgId, c, preview) {
|
|
|
95
112
|
* An org admin switches one component on or off in the org (only that component, rule 5); one of a
|
|
96
113
|
* builtin plugin -> 409 `plugin.scope_locked` (an `all` one may go off, layering spec §4); switching
|
|
97
114
|
* off something in use is previewed with its agents first.
|
|
98
|
-
* Name clashes are checked for skills
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
115
|
+
* Name clashes are checked for skills, agent templates by their roster type, and themes by their
|
|
116
|
+
* id among the themes on in an org (themes spec §6). A change that would take the platform default
|
|
117
|
+
* theme away from an org is refused (409 `theme.platform_default`, themes spec §7). Components are
|
|
118
|
+
* resolved at read time (skills/usable.ts, agents/templates.ts), so nothing is enqueued.
|
|
119
|
+
* Stateless, shared by every replica.
|
|
102
120
|
*/
|
|
103
121
|
let ComponentScopeService = class ComponentScopeService {
|
|
104
122
|
kdb;
|
|
@@ -123,13 +141,13 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
123
141
|
/**
|
|
124
142
|
* What the admin routes act on (ruling P3-23): the platform component `id`, an org row the org
|
|
125
143
|
* promoted already (its platform row), or an org's own row not promoted yet. A connector
|
|
126
|
-
* template has no org rows; anything else -> 404 `component.not_found`.
|
|
144
|
+
* template or a theme has no org rows; anything else -> 404 `component.not_found`.
|
|
127
145
|
*/
|
|
128
146
|
async resolve(kind, id) {
|
|
129
147
|
const [c] = await loadComponents(this.db, kind, { ids: [id] });
|
|
130
148
|
if (c)
|
|
131
149
|
return { c };
|
|
132
|
-
if (kind === "connector_template")
|
|
150
|
+
if (kind === "connector_template" || kind === "theme")
|
|
133
151
|
throw notFound();
|
|
134
152
|
const org = await this.promotion.orgRow(kind, id);
|
|
135
153
|
if (!org)
|
|
@@ -166,7 +184,12 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
166
184
|
const st = await loadScopeState(this.db, kind, comps);
|
|
167
185
|
const orgs = await this.liveOrgs();
|
|
168
186
|
const owners = await ownersOf(this.db, comps.flatMap((c) => (c.ownerOrgId === null ? [] : [c.ownerOrgId])));
|
|
187
|
+
// a theme's name and swatches come from its current definition (the admin pickers, §10)
|
|
188
|
+
const defs = kind === "theme"
|
|
189
|
+
? await themeDefinitions(this.db, comps.map((c) => c.id))
|
|
190
|
+
: null;
|
|
169
191
|
const platform = comps.map((c) => {
|
|
192
|
+
const def = defs?.get(c.id)?.definition;
|
|
170
193
|
const plugin = c.itemId === null ? undefined : st.plugins.get(c.itemId);
|
|
171
194
|
const follow = c.scopeMode === "follow" && c.itemId !== null;
|
|
172
195
|
const scope = follow
|
|
@@ -202,6 +225,7 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
202
225
|
offCount: scope === "all" && plugin?.builtin !== true
|
|
203
226
|
? orgs.filter((o) => switchOf(st, c.id, o.id) === false).length
|
|
204
227
|
: 0,
|
|
228
|
+
...(def !== undefined && { theme: { name: def.name, swatches: themeSwatches(def) } }),
|
|
205
229
|
};
|
|
206
230
|
});
|
|
207
231
|
const owned = await this.orgOwned(kind, orgs);
|
|
@@ -213,6 +237,8 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
213
237
|
*/
|
|
214
238
|
async orgList(orgId, filter) {
|
|
215
239
|
const out = [];
|
|
240
|
+
// the platform default theme is locked on in every org (themes spec §7)
|
|
241
|
+
const uiTheme = await platformDefaultThemeId(this.db);
|
|
216
242
|
for (const kind of filter.kind !== undefined ? [filter.kind] : COMPONENT_KINDS) {
|
|
217
243
|
const comps = await loadComponents(this.db, kind, filter.itemId !== undefined ? { itemId: filter.itemId } : {});
|
|
218
244
|
const st = await loadScopeState(this.db, kind, comps);
|
|
@@ -226,7 +252,7 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
226
252
|
const on = componentOn(c, p, listed, sw);
|
|
227
253
|
if (!on && !componentOffered(c, p, listed))
|
|
228
254
|
continue;
|
|
229
|
-
const locked = componentLocked(c, p);
|
|
255
|
+
const locked = componentLocked(c, p) || isUiTheme(c, uiTheme);
|
|
230
256
|
const pluginOn = componentPluginOn(c, p);
|
|
231
257
|
rows.push({
|
|
232
258
|
kind,
|
|
@@ -255,11 +281,11 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
255
281
|
/**
|
|
256
282
|
* The org's copies of platform skills or agent templates linked without a pin (ruling B2), by
|
|
257
283
|
* platform id: they resolve to it, so agents that select them use it (none for connector
|
|
258
|
-
* templates).
|
|
284
|
+
* templates and themes).
|
|
259
285
|
*/
|
|
260
286
|
async linkedCopies(orgId, kind) {
|
|
261
287
|
const out = new Map();
|
|
262
|
-
if (kind === "connector_template")
|
|
288
|
+
if (kind === "connector_template" || kind === "theme")
|
|
263
289
|
return out;
|
|
264
290
|
const rows = kind === "skill"
|
|
265
291
|
? await this.db
|
|
@@ -289,10 +315,11 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
289
315
|
}
|
|
290
316
|
/**
|
|
291
317
|
* The orgs' own skills (org scope) and agent templates, not linked to a platform component
|
|
292
|
-
* (ruling P2-11): listed read-only, on in their org only. Connector templates have none
|
|
318
|
+
* (ruling P2-11): listed read-only, on in their org only. Connector templates have none; an
|
|
319
|
+
* org's own theme is a platform_themes row with its org as owner, listed with the platform ones.
|
|
293
320
|
*/
|
|
294
321
|
async orgOwned(kind, orgs) {
|
|
295
|
-
if (kind === "connector_template")
|
|
322
|
+
if (kind === "connector_template" || kind === "theme")
|
|
296
323
|
return [];
|
|
297
324
|
const live = new Set(orgs.map((o) => o.id));
|
|
298
325
|
const rows = kind === "skill"
|
|
@@ -428,7 +455,8 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
428
455
|
* The org admin's switch of one component (`PUT orgs/:orgId/components/:kind/:id/switch`): only
|
|
429
456
|
* this component (rule 5). A component not offered to the org -> 404. On: refused while the
|
|
430
457
|
* component or its plugin is disabled (`plugin.disabled`) or an own skill of the org has its name
|
|
431
|
-
* (`plugin.skill_conflict`, rule 10). Off:
|
|
458
|
+
* (`plugin.skill_conflict`, rule 10; a theme: `theme.id_taken`). Off: the platform default theme
|
|
459
|
+
* while it is on -> `theme.platform_default`; of a builtin plugin -> `plugin.scope_locked`; while it
|
|
432
460
|
* is on, without `confirm` the preview (agents that use it) and its hash are returned, with the
|
|
433
461
|
* current hash the switch is written (a changed preview -> `plugin.switch_stale`).
|
|
434
462
|
*/
|
|
@@ -459,7 +487,26 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
459
487
|
throw new ApiError("plugin.disabled", "This component is disabled by the super admin and cannot be switched on");
|
|
460
488
|
}
|
|
461
489
|
if (body.enabled) {
|
|
462
|
-
if (kind
|
|
490
|
+
if (kind === "theme") {
|
|
491
|
+
// theme ids are unique among the themes on in the org (themes spec §6)
|
|
492
|
+
if (!on && componentOn(c, p, listed, true)) {
|
|
493
|
+
const clash = await themeIdClashes(this.db, orgId, c, c.name);
|
|
494
|
+
if (clash.length > 0) {
|
|
495
|
+
const [org] = await this.liveOrgs(orgId);
|
|
496
|
+
throw nameConflictError([
|
|
497
|
+
{
|
|
498
|
+
orgId,
|
|
499
|
+
orgName: org?.name ?? orgId,
|
|
500
|
+
name: c.name,
|
|
501
|
+
skills: [],
|
|
502
|
+
themes: clash.map((x) => x.id),
|
|
503
|
+
plugins: clash.map((x) => x.plugin ?? "platform"),
|
|
504
|
+
},
|
|
505
|
+
], kind);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else if (kind !== "connector_template" && !on) {
|
|
463
510
|
const clash = kind === "skill"
|
|
464
511
|
? await skillNameClashes(this.db, orgId, c, c.name)
|
|
465
512
|
: await templateTypeClashes(this.db, orgId, c);
|
|
@@ -471,13 +518,22 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
471
518
|
}
|
|
472
519
|
}
|
|
473
520
|
else {
|
|
521
|
+
// The platform default theme is locked on in every org (themes spec §7): refused before the
|
|
522
|
+
// preview, inside this transaction
|
|
523
|
+
if (kind === "theme" && on)
|
|
524
|
+
await assertNotPlatformDefault(this.db, [c.id], () => "narrower");
|
|
474
525
|
if (componentLocked(c, p)) {
|
|
475
526
|
throw new ApiError("plugin.scope_locked", "This component belongs to a built-in plugin: it is on for every organization and cannot be switched off here");
|
|
476
527
|
}
|
|
477
528
|
if (on) {
|
|
529
|
+
const themes = kind === "theme"
|
|
530
|
+
? await themesPreview(this.db, new Map([[c.id, new Set([orgId])]]))
|
|
531
|
+
: undefined;
|
|
478
532
|
const preview = {
|
|
479
533
|
agents: await agentsUsing(this.db, orgId, kind, [c.id]),
|
|
480
534
|
connectorsKept: kind === "connector_template" ? await connectorsMadeFrom(this.db, orgId, c.id) : 0,
|
|
535
|
+
// a theme only: other kinds keep the preview (and the hash) they had before themes
|
|
536
|
+
...(themes !== undefined && { themes }),
|
|
481
537
|
};
|
|
482
538
|
const confirm = switchHash(orgId, c, preview);
|
|
483
539
|
if (body.confirm === undefined)
|
|
@@ -528,7 +584,7 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
528
584
|
plugin: c.plugin,
|
|
529
585
|
switch: body.enabled,
|
|
530
586
|
on: componentOn(c, p, listed, body.enabled),
|
|
531
|
-
locked: componentLocked(c, p),
|
|
587
|
+
locked: componentLocked(c, p) || isUiTheme(c, await platformDefaultThemeId(this.db)),
|
|
532
588
|
},
|
|
533
589
|
};
|
|
534
590
|
}
|
|
@@ -551,7 +607,7 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
551
607
|
const offered = componentOffered({ ...c, disabled: false }, p && { ...p, disabled: false }, listed);
|
|
552
608
|
if (!on && !offered)
|
|
553
609
|
throw notFound();
|
|
554
|
-
const locked = componentLocked(c, p);
|
|
610
|
+
const locked = componentLocked(c, p) || isUiTheme(c, await platformDefaultThemeId(this.db));
|
|
555
611
|
let preview = null;
|
|
556
612
|
let confirm = null;
|
|
557
613
|
if (on && !locked) {
|
|
@@ -578,6 +634,8 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
578
634
|
id = await this.promotion.promote(x.promote, actor);
|
|
579
635
|
const c = await this.component(kind, id, true);
|
|
580
636
|
const t = await this.targetOf(c, target);
|
|
637
|
+
if (kind === "theme")
|
|
638
|
+
await this.assertKeepsPlatformDefault(c, t);
|
|
581
639
|
const from = await this.listed(kind, id);
|
|
582
640
|
// Its own scope: the owner stays listed (P3-17), widening goes through the share gate (P3-16);
|
|
583
641
|
// following the plugin again takes the plugin's scope, which its own writer gates
|
|
@@ -615,8 +673,9 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
615
673
|
.values(t.orgs.map((org_id) => ({ kind, component_id: id, org_id })))
|
|
616
674
|
.execute();
|
|
617
675
|
}
|
|
618
|
-
// An own scope that newly shows an org-owned component tells its owner (P3-22)
|
|
619
|
-
|
|
676
|
+
// An own scope that newly shows an org-owned component tells its owner (P3-22); not a theme:
|
|
677
|
+
// the `item_shared` notice names plugins, skills and agent templates only
|
|
678
|
+
if (t.scopeMode === "own" && kind !== "connector_template" && kind !== "theme") {
|
|
620
679
|
const shared = newlySharedWith(before, t);
|
|
621
680
|
if (shared === "all_orgs" || shared.length > 0) {
|
|
622
681
|
await notifyItemShared(this.db, c.ownerOrgId, { kind, id }, c.name, shared);
|
|
@@ -643,7 +702,10 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
643
702
|
const c = await this.component(kind, id, true);
|
|
644
703
|
if (c.disabled === disabled)
|
|
645
704
|
return;
|
|
646
|
-
const
|
|
705
|
+
const t = await this.targetOf(c, { disabled });
|
|
706
|
+
if (kind === "theme")
|
|
707
|
+
await this.assertKeepsPlatformDefault(c, t);
|
|
708
|
+
const p = await this.compute(c, t);
|
|
647
709
|
if (disabled && p.scopeHash !== hash)
|
|
648
710
|
throw staleError();
|
|
649
711
|
if (p.conflicts.length > 0)
|
|
@@ -737,12 +799,19 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
737
799
|
.where("skill_id", "=", id)
|
|
738
800
|
.orderBy("version", "desc")
|
|
739
801
|
.execute()
|
|
740
|
-
:
|
|
741
|
-
.
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
802
|
+
: kind === "theme"
|
|
803
|
+
? await this.db
|
|
804
|
+
.selectFrom("platform_theme_versions")
|
|
805
|
+
.select(["version", "catalog_version", "updated_on_own", "created_at"])
|
|
806
|
+
.where("theme_id", "=", id)
|
|
807
|
+
.orderBy("version", "desc")
|
|
808
|
+
.execute()
|
|
809
|
+
: await this.db
|
|
810
|
+
.selectFrom("platform_agent_template_versions")
|
|
811
|
+
.select(["version", "catalog_version", "updated_on_own", "created_at"])
|
|
812
|
+
.where("template_id", "=", id)
|
|
813
|
+
.orderBy("version", "desc")
|
|
814
|
+
.execute();
|
|
746
815
|
return rows.map((r) => ({
|
|
747
816
|
version: String(r.version),
|
|
748
817
|
catalogVersion: r.catalog_version,
|
|
@@ -860,6 +929,36 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
860
929
|
await assertOrgsExist(this.db, orgs);
|
|
861
930
|
return { scopeMode: "own", scope: target.scope, orgs, disabled: c.disabled };
|
|
862
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* 409 `theme.platform_default` when theme `c` is the platform default and `t` would leave it
|
|
934
|
+
* narrower than `all` or gone (themes spec §7), in the caller's transaction, before any write.
|
|
935
|
+
*/
|
|
936
|
+
async assertKeepsPlatformDefault(c, t) {
|
|
937
|
+
const after = await this.themeAfter(c, t);
|
|
938
|
+
await assertNotPlatformDefault(this.db, [c.id], () => after);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Theme `c`'s availability under `t`: gone when disabled (or its plugin is disabled or deleted
|
|
942
|
+
* while it follows it), else `all` when its effective scope (its own, or its plugin's; builtin:
|
|
943
|
+
* `all`) is `all`, else narrower.
|
|
944
|
+
*/
|
|
945
|
+
async themeAfter(c, t) {
|
|
946
|
+
if (t.disabled)
|
|
947
|
+
return "gone";
|
|
948
|
+
if (t.scopeMode === "own")
|
|
949
|
+
return t.scope === "all" ? "all" : "narrower";
|
|
950
|
+
if (c.itemId === null)
|
|
951
|
+
return "gone";
|
|
952
|
+
const item = await this.db
|
|
953
|
+
.selectFrom("catalog_items")
|
|
954
|
+
.select(["scope", "builtin", "disabled"])
|
|
955
|
+
.where("id", "=", c.itemId)
|
|
956
|
+
.where("deleted_at", "is", null)
|
|
957
|
+
.executeTakeFirst();
|
|
958
|
+
if (!item || item.disabled)
|
|
959
|
+
return "gone";
|
|
960
|
+
return item.builtin || item.scope === "all" ? "all" : "narrower";
|
|
961
|
+
}
|
|
863
962
|
/** Live orgs (`only`: that one), sorted by id. */
|
|
864
963
|
async liveOrgs(only) {
|
|
865
964
|
let q = this.db
|
|
@@ -894,11 +993,16 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
894
993
|
const listedAfter = new Set(target.orgs);
|
|
895
994
|
const affected = [];
|
|
896
995
|
const conflicts = [];
|
|
996
|
+
/** A theme: the orgs it is on in now and would be off in. */
|
|
997
|
+
const lost = new Set();
|
|
897
998
|
for (const org of await this.liveOrgs()) {
|
|
898
999
|
const p = pluginFactsOf(st, c.itemId, org.id);
|
|
899
1000
|
const listed = listedIn(st, c.id, org.id);
|
|
900
1001
|
const sw = switchOf(st, c.id, org.id);
|
|
901
|
-
|
|
1002
|
+
const onNow = componentOn(c, p, listed, sw);
|
|
1003
|
+
const onAfter = componentOn(after, p, listedAfter.has(org.id), sw);
|
|
1004
|
+
if (onNow && !onAfter) {
|
|
1005
|
+
lost.add(org.id);
|
|
902
1006
|
const agents = await agentsUsing(this.db, org.id, c.kind, [c.id]);
|
|
903
1007
|
affected.push({
|
|
904
1008
|
orgId: org.id,
|
|
@@ -907,6 +1011,23 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
907
1011
|
connectorsKept: c.kind === "connector_template" ? await connectorsMadeFrom(this.db, org.id, c.id) : 0,
|
|
908
1012
|
});
|
|
909
1013
|
}
|
|
1014
|
+
if (c.kind === "theme") {
|
|
1015
|
+
// theme ids are unique among the themes on in an org (themes spec §6)
|
|
1016
|
+
if (onAfter && !onNow) {
|
|
1017
|
+
const clash = await themeIdClashes(this.db, org.id, c, c.name);
|
|
1018
|
+
if (clash.length > 0) {
|
|
1019
|
+
conflicts.push({
|
|
1020
|
+
orgId: org.id,
|
|
1021
|
+
orgName: org.name,
|
|
1022
|
+
name: c.name,
|
|
1023
|
+
skills: [],
|
|
1024
|
+
themes: clash.map((x) => x.id),
|
|
1025
|
+
plugins: clash.map((x) => x.plugin ?? "platform"),
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
910
1031
|
if (c.kind !== "connector_template" &&
|
|
911
1032
|
componentOffered(after, p, listedAfter.has(org.id)) &&
|
|
912
1033
|
!componentOffered(c, p, listed)) {
|
|
@@ -920,7 +1041,8 @@ let ComponentScopeService = class ComponentScopeService {
|
|
|
920
1041
|
}
|
|
921
1042
|
}
|
|
922
1043
|
}
|
|
923
|
-
const
|
|
1044
|
+
const themes = c.kind === "theme" ? await themesPreview(this.db, new Map([[c.id, lost]])) : undefined;
|
|
1045
|
+
const p = { affected, conflicts, ...(themes !== undefined && { themes }) };
|
|
924
1046
|
return { ...p, scopeHash: previewHash({ kind: c.kind, id: x.hashId ?? c.id }, target, p) };
|
|
925
1047
|
}
|
|
926
1048
|
};
|
|
@@ -945,6 +1067,8 @@ ComponentScopeService = __decorate([
|
|
|
945
1067
|
__param(4, Inject(TransactionHost))
|
|
946
1068
|
], ComponentScopeService);
|
|
947
1069
|
export { ComponentScopeService };
|
|
1070
|
+
/** Whether `c` is the platform default theme `uiTheme` (locked on in every org, themes spec §7). */
|
|
1071
|
+
const isUiTheme = (c, uiTheme) => c.kind === "theme" && c.id === uiTheme;
|
|
948
1072
|
/** The plugin row FOR SHARE (exclusive with its scope writers and an unshare, which hold it FOR UPDATE). */
|
|
949
1073
|
async function lockItemShared(db, itemId) {
|
|
950
1074
|
await db.selectFrom("catalog_items").select("id").where("id", "=", itemId).forShare().execute();
|
|
@@ -987,4 +1111,5 @@ const TABLE = {
|
|
|
987
1111
|
skill: "platform_skills",
|
|
988
1112
|
agent_template: "platform_agent_templates",
|
|
989
1113
|
connector_template: "platform_connector_templates",
|
|
1114
|
+
theme: "platform_themes",
|
|
990
1115
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { KernelSchemaDb } from "../db/kernel-schema.js";
|
|
2
2
|
import { type PluginScope } from "./scope.js";
|
|
3
3
|
/** The kinds of platform components. */
|
|
4
|
-
export type ComponentKind = "skill" | "agent_template" | "connector_template";
|
|
4
|
+
export type ComponentKind = "skill" | "agent_template" | "connector_template" | "theme";
|
|
5
|
+
/** The kinds the scope, switch, ownership and preview flows (and the component routes) walk. */
|
|
5
6
|
export declare const COMPONENT_KINDS: readonly ComponentKind[];
|
|
6
7
|
/** A component's own scope settings (ruling C2). */
|
|
7
8
|
export interface ComponentScopeFacts {
|
|
@@ -70,7 +71,7 @@ export interface ComponentRow extends ComponentScopeFacts {
|
|
|
70
71
|
shownToOthers: boolean;
|
|
71
72
|
/** The owning org (null: the platform; ruling P3-1). */
|
|
72
73
|
ownerOrgId: string | null;
|
|
73
|
-
/** The org it was created in (skills, agent templates; null: the platform, F27). */
|
|
74
|
+
/** The org it was created in (skills, agent templates, themes; null: the platform, F27). */
|
|
74
75
|
originOrgId: string | null;
|
|
75
76
|
/** Its owner shares it with other orgs (the share gate, P3-1). */
|
|
76
77
|
shareable: boolean;
|
|
@@ -130,7 +131,8 @@ export declare function agentDefinitionRefs(db: KernelSchemaDb, orgId: string):
|
|
|
130
131
|
/**
|
|
131
132
|
* The ids an org's agents may reference a platform component by: its own id and the org's copies
|
|
132
133
|
* linked to it (ruling B2: unpinned ones resolve to it; ruling P2-18: pinned ones follow its
|
|
133
|
-
* availability). Connector templates: none (connectors made from one stay the org's own)
|
|
134
|
+
* availability). Connector templates: none (connectors made from one stay the org's own); themes:
|
|
135
|
+
* none (no agent references a theme, and a theme has no org copies).
|
|
134
136
|
*/
|
|
135
137
|
export declare function referenceIds(db: KernelSchemaDb, orgId: string, kind: ComponentKind, ids: readonly string[]): Promise<Map<string, string>>;
|
|
136
138
|
/**
|
|
@@ -167,6 +169,13 @@ export declare function skillNameClashes(db: KernelSchemaDb, orgId: string, comp
|
|
|
167
169
|
skills: string[];
|
|
168
170
|
plugins: string[];
|
|
169
171
|
}>;
|
|
172
|
+
/**
|
|
173
|
+
* The live themes with id `name` that are on in the org (themes spec §6: theme ids are unique in an
|
|
174
|
+
* org's usable set, rule 10), except `self` (null: a theme with no row yet, e.g. one an update adds).
|
|
175
|
+
*/
|
|
176
|
+
export declare function themeIdClashes(db: KernelSchemaDb, orgId: string, self: {
|
|
177
|
+
id: string;
|
|
178
|
+
} | null, name: string): Promise<ComponentRow[]>;
|
|
170
179
|
/** A connector template's id from its manifest path (`./connectors/<id>.json`). */
|
|
171
180
|
export declare const templateIdOfPath: (path: string) => string;
|
|
172
181
|
/**
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { visibleTo, wantedOn } from "./scope.js";
|
|
2
|
+
/** The kinds the scope, switch, ownership and preview flows (and the component routes) walk. */
|
|
2
3
|
export const COMPONENT_KINDS = [
|
|
3
4
|
"skill",
|
|
4
5
|
"agent_template",
|
|
5
6
|
"connector_template",
|
|
7
|
+
"theme",
|
|
6
8
|
];
|
|
7
9
|
/** The component or its plugin stops it everywhere: disabled, or the plugin disabled or deleted. */
|
|
8
10
|
function stopped(c, p) {
|
|
@@ -115,7 +117,11 @@ export async function loadComponents(db, kind, filter = {}) {
|
|
|
115
117
|
originOrgId: null,
|
|
116
118
|
}));
|
|
117
119
|
}
|
|
118
|
-
const table = kind === "skill"
|
|
120
|
+
const table = kind === "skill"
|
|
121
|
+
? "platform_skills"
|
|
122
|
+
: kind === "theme"
|
|
123
|
+
? "platform_themes"
|
|
124
|
+
: "platform_agent_templates";
|
|
119
125
|
let q = db
|
|
120
126
|
.selectFrom(`${table} as p`)
|
|
121
127
|
.select([
|
|
@@ -308,11 +314,12 @@ export async function agentDefinitionRefs(db, orgId) {
|
|
|
308
314
|
/**
|
|
309
315
|
* The ids an org's agents may reference a platform component by: its own id and the org's copies
|
|
310
316
|
* linked to it (ruling B2: unpinned ones resolve to it; ruling P2-18: pinned ones follow its
|
|
311
|
-
* availability). Connector templates: none (connectors made from one stay the org's own)
|
|
317
|
+
* availability). Connector templates: none (connectors made from one stay the org's own); themes:
|
|
318
|
+
* none (no agent references a theme, and a theme has no org copies).
|
|
312
319
|
*/
|
|
313
320
|
export async function referenceIds(db, orgId, kind, ids) {
|
|
314
321
|
const out = new Map();
|
|
315
|
-
if (kind === "connector_template" || ids.length === 0)
|
|
322
|
+
if (kind === "connector_template" || kind === "theme" || ids.length === 0)
|
|
316
323
|
return out;
|
|
317
324
|
for (const id of ids)
|
|
318
325
|
out.set(id, id);
|
|
@@ -402,6 +409,17 @@ export async function skillNameClashes(db, orgId, component, name) {
|
|
|
402
409
|
plugins: platform.map((c) => c.plugin ?? "platform"),
|
|
403
410
|
};
|
|
404
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* The live themes with id `name` that are on in the org (themes spec §6: theme ids are unique in an
|
|
414
|
+
* org's usable set, rule 10), except `self` (null: a theme with no row yet, e.g. one an update adds).
|
|
415
|
+
*/
|
|
416
|
+
export async function themeIdClashes(db, orgId, self, name) {
|
|
417
|
+
const others = (await loadComponents(db, "theme", { name })).filter((c) => c.id !== self?.id);
|
|
418
|
+
if (others.length === 0)
|
|
419
|
+
return [];
|
|
420
|
+
const st = await loadScopeState(db, "theme", others);
|
|
421
|
+
return others.filter((c) => componentOn(c, pluginFactsOf(st, c.itemId, orgId), listedIn(st, c.id, orgId), switchOf(st, c.id, orgId)));
|
|
422
|
+
}
|
|
405
423
|
/** A connector template's id from its manifest path (`./connectors/<id>.json`). */
|
|
406
424
|
export const templateIdOfPath = (path) => (path.split("/").pop() ?? path).replace(/\.json$/, "");
|
|
407
425
|
/**
|
|
@@ -188,7 +188,7 @@ export declare function headsToResolve(entries: readonly MarketplaceEntry[], imp
|
|
|
188
188
|
/** The host versions (an entry's `minKernel` is judged against them). */
|
|
189
189
|
export declare const HOST_VERSIONS: {
|
|
190
190
|
readonly kernel: string;
|
|
191
|
-
readonly sdk: "0.1.0-rc.
|
|
191
|
+
readonly sdk: "0.1.0-rc.8";
|
|
192
192
|
};
|
|
193
193
|
/** Whether this deployment satisfies the entry's `minKernel`. */
|
|
194
194
|
export declare const entryCompatible: (e: Pick<MarketplaceEntry, "minKernel">) => boolean;
|
|
@@ -479,6 +479,9 @@ let OrgCatalogService = class OrgCatalogService {
|
|
|
479
479
|
await this.recordSwitch(orgId, actor, itemId, false);
|
|
480
480
|
return { kind: "done", item: await this.itemOf(orgId, actor, itemId) };
|
|
481
481
|
}
|
|
482
|
+
// The platform default theme is locked on (themes spec §7): refused at the preview already;
|
|
483
|
+
// the uninstall's transaction checks again before it writes
|
|
484
|
+
await this.pluginInstaller.assertKeepsPlatformDefault(orgId, live.id);
|
|
482
485
|
const preview = await this.pluginInstaller.previewUninstall(orgId, live.id);
|
|
483
486
|
const confirm = uninstallPreviewHash(preview);
|
|
484
487
|
if (body.confirm === undefined)
|