@frockbot/plugin-settings 0.2.4 → 0.2.5
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/package.json +9 -9
- package/src/client/PluginsSurface.vue +15 -7
- package/src/user.test.ts +174 -0
- package/src/user.ts +172 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-settings",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
"typecheck": "vue-tsc --noEmit -p tsconfig.json"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@frockbot/catalog-core": "0.2.
|
|
25
|
-
"@frockbot/client-core": "0.2.
|
|
26
|
-
"@frockbot/client-ui": "0.2.
|
|
27
|
-
"@frockbot/configuration-core": "0.2.
|
|
28
|
-
"@frockbot/connection-core": "0.2.
|
|
29
|
-
"@frockbot/kernel-composition": "0.2.
|
|
30
|
-
"@frockbot/plugin-auth": "0.2.
|
|
31
|
-
"@frockbot/plugin-shell": "0.2.
|
|
24
|
+
"@frockbot/catalog-core": "0.2.5",
|
|
25
|
+
"@frockbot/client-core": "0.2.5",
|
|
26
|
+
"@frockbot/client-ui": "0.2.5",
|
|
27
|
+
"@frockbot/configuration-core": "0.2.5",
|
|
28
|
+
"@frockbot/connection-core": "0.2.5",
|
|
29
|
+
"@frockbot/kernel-composition": "0.2.5",
|
|
30
|
+
"@frockbot/plugin-auth": "0.2.5",
|
|
31
|
+
"@frockbot/plugin-shell": "0.2.5",
|
|
32
32
|
"cordis": "4.0.0-rc.8",
|
|
33
33
|
"vue": "3.5.41"
|
|
34
34
|
},
|
|
@@ -37,16 +37,19 @@ const search = ref("");
|
|
|
37
37
|
const busyPackageId = ref<string>();
|
|
38
38
|
|
|
39
39
|
const installations = computed(() => web.value.userSettings?.packages ?? []);
|
|
40
|
+
const userPackages = computed(() =>
|
|
41
|
+
web.value.pluginCatalog.filter((item) => !item.platformOwned),
|
|
42
|
+
);
|
|
40
43
|
const installedPluginCount = computed(
|
|
41
44
|
() =>
|
|
42
|
-
|
|
45
|
+
userPackages.value.filter((item) =>
|
|
43
46
|
isPackageInstalled(installations.value, item.packageId),
|
|
44
47
|
).length,
|
|
45
48
|
);
|
|
46
49
|
const filteredCatalog = computed(() => {
|
|
47
50
|
const query = search.value.trim().toLocaleLowerCase();
|
|
48
|
-
if (!query) return
|
|
49
|
-
return
|
|
51
|
+
if (!query) return userPackages.value;
|
|
52
|
+
return userPackages.value.filter(
|
|
50
53
|
(item) =>
|
|
51
54
|
item.displayName.toLocaleLowerCase().includes(query) ||
|
|
52
55
|
item.packageId.toLocaleLowerCase().includes(query),
|
|
@@ -143,6 +146,14 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
|
|
|
143
146
|
</span>
|
|
144
147
|
{{ installedPluginCount }} installed
|
|
145
148
|
</div>
|
|
149
|
+
<!--
|
|
150
|
+
A refused command is the answer to the click that caused it, so it is
|
|
151
|
+
shown above the list where the click happened, not below a long grid
|
|
152
|
+
where it scrolls out of view.
|
|
153
|
+
-->
|
|
154
|
+
<p v-if="web.settingsError" class="settings-error" role="alert">
|
|
155
|
+
{{ web.settingsError }}
|
|
156
|
+
</p>
|
|
146
157
|
<label class="plugin-search">
|
|
147
158
|
<UiIcon name="search" />
|
|
148
159
|
<input
|
|
@@ -223,7 +234,7 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
|
|
|
223
234
|
</div>
|
|
224
235
|
|
|
225
236
|
<p
|
|
226
|
-
v-if="
|
|
237
|
+
v-if="userPackages.length === 0 && !web.settingsError"
|
|
227
238
|
class="plugin-empty"
|
|
228
239
|
>
|
|
229
240
|
This application ships no Packages.
|
|
@@ -231,9 +242,6 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
|
|
|
231
242
|
<p v-else-if="filteredCatalog.length === 0" class="plugin-empty">
|
|
232
243
|
No Package matches that search.
|
|
233
244
|
</p>
|
|
234
|
-
<p v-if="web.settingsError" class="settings-error" role="alert">
|
|
235
|
-
{{ web.settingsError }}
|
|
236
|
-
</p>
|
|
237
245
|
</div>
|
|
238
246
|
</template>
|
|
239
247
|
|
package/src/user.test.ts
CHANGED
|
@@ -407,6 +407,180 @@ describe("User settings backend Contribution", () => {
|
|
|
407
407
|
).resolves.toMatchObject({ status: "applied" });
|
|
408
408
|
});
|
|
409
409
|
|
|
410
|
+
test("seeds rollout dependencies before validating a pre-existing enablement graph", async () => {
|
|
411
|
+
const storage = new MemoryStorage();
|
|
412
|
+
const availablePackages: AvailableUserPackage[] = [
|
|
413
|
+
{
|
|
414
|
+
packageId: "custom-models",
|
|
415
|
+
version: "0.0.1",
|
|
416
|
+
installByDefault: true,
|
|
417
|
+
defaultEnablement: "disabled",
|
|
418
|
+
dependencies: {
|
|
419
|
+
settings: ">=0.0.1",
|
|
420
|
+
shell: ">=0.0.1",
|
|
421
|
+
"ui-theme": ">=0.0.1",
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
packageId: "provider-ollama-cloud",
|
|
426
|
+
version: "0.0.1",
|
|
427
|
+
installByDefault: true,
|
|
428
|
+
defaultEnablement: "disabled",
|
|
429
|
+
dependencies: {
|
|
430
|
+
"custom-models": ">=0.0.1",
|
|
431
|
+
credentials: ">=0.0.1",
|
|
432
|
+
settings: ">=0.0.1",
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
{ packageId: "credentials", version: "0.0.1", installByDefault: true },
|
|
436
|
+
{
|
|
437
|
+
packageId: "settings",
|
|
438
|
+
version: "0.0.1",
|
|
439
|
+
installByDefault: true,
|
|
440
|
+
dependencies: { shell: ">=0.0.1", "ui-theme": ">=0.0.1" },
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
packageId: "shell",
|
|
444
|
+
version: "0.0.1",
|
|
445
|
+
installByDefault: true,
|
|
446
|
+
dependencies: { "ui-theme": ">=0.0.1" },
|
|
447
|
+
},
|
|
448
|
+
{ packageId: "ui-theme", version: "0.0.1", installByDefault: true },
|
|
449
|
+
{
|
|
450
|
+
packageId: "mcp",
|
|
451
|
+
version: "0.0.1",
|
|
452
|
+
installByDefault: true,
|
|
453
|
+
dependencies: {
|
|
454
|
+
credentials: ">=0.0.1",
|
|
455
|
+
settings: ">=0.0.1",
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
];
|
|
459
|
+
await storage.put("user-id", "pre-rollout-user");
|
|
460
|
+
await storage.put("user-default-packages-bootstrap:v1", {
|
|
461
|
+
schemaVersion: 1,
|
|
462
|
+
});
|
|
463
|
+
await storage.put("user-configuration", {
|
|
464
|
+
schemaVersion: 1,
|
|
465
|
+
revision: 12,
|
|
466
|
+
profile: { name: "Pre-rollout User" },
|
|
467
|
+
packages: [
|
|
468
|
+
{
|
|
469
|
+
packageId: "shell",
|
|
470
|
+
version: "0.0.1",
|
|
471
|
+
state: "installed",
|
|
472
|
+
provenance: "first-party",
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
packageId: "mcp",
|
|
476
|
+
version: "0.0.1",
|
|
477
|
+
state: "installed",
|
|
478
|
+
provenance: "first-party",
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
packageId: "provider-ollama-cloud",
|
|
482
|
+
version: "0.0.1",
|
|
483
|
+
state: "installed",
|
|
484
|
+
provenance: "first-party",
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
connections: [],
|
|
488
|
+
});
|
|
489
|
+
const settings = createUserSettingsBackendContribution({
|
|
490
|
+
storage,
|
|
491
|
+
availablePackages,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const migrated = await settings.readConfiguration({
|
|
495
|
+
schemaVersion: 1,
|
|
496
|
+
userId: "pre-rollout-user",
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
expect(migrated.packages).toContainEqual(
|
|
500
|
+
expect.objectContaining({ packageId: "shell", state: "installed" }),
|
|
501
|
+
);
|
|
502
|
+
expect(migrated.packages).toContainEqual(
|
|
503
|
+
expect.objectContaining({ packageId: "mcp", state: "installed" }),
|
|
504
|
+
);
|
|
505
|
+
expect(migrated.packages).toContainEqual(
|
|
506
|
+
expect.objectContaining({
|
|
507
|
+
packageId: "custom-models",
|
|
508
|
+
state: "disabled",
|
|
509
|
+
}),
|
|
510
|
+
);
|
|
511
|
+
expect(migrated.packages).toContainEqual(
|
|
512
|
+
expect.objectContaining({
|
|
513
|
+
packageId: "provider-ollama-cloud",
|
|
514
|
+
state: "disabled",
|
|
515
|
+
}),
|
|
516
|
+
);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("repairs v2 rollout damage once and preserves choices made after v3", async () => {
|
|
520
|
+
const storage = new MemoryStorage();
|
|
521
|
+
await storage.put("user-id", "damaged-user");
|
|
522
|
+
await storage.put("user-default-packages-bootstrap:v1", {
|
|
523
|
+
schemaVersion: 2,
|
|
524
|
+
});
|
|
525
|
+
await storage.put("user-configuration", {
|
|
526
|
+
schemaVersion: 1,
|
|
527
|
+
revision: 38,
|
|
528
|
+
profile: { name: "Damaged User" },
|
|
529
|
+
packages: [
|
|
530
|
+
{
|
|
531
|
+
packageId: "web",
|
|
532
|
+
version: "0.0.1",
|
|
533
|
+
state: "disabled",
|
|
534
|
+
provenance: "first-party",
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
connections: [],
|
|
538
|
+
});
|
|
539
|
+
const settings = createUserSettingsBackendContribution({
|
|
540
|
+
storage,
|
|
541
|
+
availablePackages: [
|
|
542
|
+
{ packageId: "web", version: "0.0.1", installByDefault: true },
|
|
543
|
+
],
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const repaired = await settings.readConfiguration({
|
|
547
|
+
schemaVersion: 1,
|
|
548
|
+
userId: "damaged-user",
|
|
549
|
+
});
|
|
550
|
+
expect(repaired).toMatchObject({
|
|
551
|
+
revision: 39,
|
|
552
|
+
packages: [{ packageId: "web", state: "installed" }],
|
|
553
|
+
});
|
|
554
|
+
expect(
|
|
555
|
+
await storage.get<{ schemaVersion: number }>(
|
|
556
|
+
"user-default-packages-bootstrap:v1",
|
|
557
|
+
),
|
|
558
|
+
).toEqual({
|
|
559
|
+
schemaVersion: 3,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
await settings.executeConfiguration({
|
|
563
|
+
schemaVersion: 1,
|
|
564
|
+
userId: "damaged-user",
|
|
565
|
+
command: {
|
|
566
|
+
schemaVersion: 1,
|
|
567
|
+
type: "user/set-package-enabled",
|
|
568
|
+
commandId: "disable-web-after-v3",
|
|
569
|
+
expectedRevision: 39,
|
|
570
|
+
packageId: "web",
|
|
571
|
+
enabled: false,
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
const chosen = await settings.readConfiguration({
|
|
575
|
+
schemaVersion: 1,
|
|
576
|
+
userId: "damaged-user",
|
|
577
|
+
});
|
|
578
|
+
expect(chosen).toMatchObject({
|
|
579
|
+
revision: 40,
|
|
580
|
+
packages: [{ packageId: "web", state: "disabled" }],
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
410
584
|
test("installs a Package disabled when explicitly requested", async () => {
|
|
411
585
|
const settings = contribution();
|
|
412
586
|
const receipt = await settings.executeConfiguration({
|
package/src/user.ts
CHANGED
|
@@ -34,7 +34,7 @@ import type { Plugin } from "cordis";
|
|
|
34
34
|
|
|
35
35
|
const STATE_KEY = "user-configuration";
|
|
36
36
|
const DEFAULT_PACKAGES_BOOTSTRAP_KEY = "user-default-packages-bootstrap:v1";
|
|
37
|
-
const DEFAULT_PACKAGES_BOOTSTRAP_VERSION =
|
|
37
|
+
const DEFAULT_PACKAGES_BOOTSTRAP_VERSION = 3;
|
|
38
38
|
/**
|
|
39
39
|
* The pinned Catalog generation lives beside the settings view rather than in
|
|
40
40
|
* it, so pinning on a read never bumps the settings revision a client is
|
|
@@ -132,6 +132,8 @@ export interface AvailableUserPackage {
|
|
|
132
132
|
installByDefault?: boolean;
|
|
133
133
|
/** The seeded installation state. Omission preserves the enabled default. */
|
|
134
134
|
defaultEnablement?: "enabled" | "disabled";
|
|
135
|
+
/** Derived from immutable manifest facts by the application compiler. */
|
|
136
|
+
platformOwned?: boolean;
|
|
135
137
|
/** The Package manifest's Package-id-to-version-range dependency record. */
|
|
136
138
|
dependencies?: Readonly<Record<string, string>>;
|
|
137
139
|
/**
|
|
@@ -402,6 +404,9 @@ export class UserSettingsBackendContribution {
|
|
|
402
404
|
/** Catalog-relative facts supplied to the raw stored-settings migration. */
|
|
403
405
|
private readonly storedSettingsPackages: readonly StoredUserSettingsPackageV1[];
|
|
404
406
|
|
|
407
|
+
/** Package ids whose installation state is platform policy, not a User choice. */
|
|
408
|
+
private readonly platformOwnedPackageIds: ReadonlySet<string>;
|
|
409
|
+
|
|
405
410
|
/** Declared Package dependencies, by Package id and version. */
|
|
406
411
|
private readonly packageDependencies: ReadonlyMap<
|
|
407
412
|
string,
|
|
@@ -418,6 +423,16 @@ export class UserSettingsBackendContribution {
|
|
|
418
423
|
*/
|
|
419
424
|
private readonly enablementRolloutPackages: readonly PackageInstallationView[];
|
|
420
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Default-enabled rows damaged by v0.2.3's v1→v2 migration. The old pass
|
|
428
|
+
* validated dependencies before it added the rollout's dependency closure,
|
|
429
|
+
* so it wrote these rows disabled even though the User had made no choice.
|
|
430
|
+
*/
|
|
431
|
+
private readonly defaultEnabledPackages: ReadonlyMap<
|
|
432
|
+
string,
|
|
433
|
+
PackageInstallationView
|
|
434
|
+
>;
|
|
435
|
+
|
|
421
436
|
/** Declared User-level settings, by Package id and version. */
|
|
422
437
|
private readonly packageSettingDefinitions: ReadonlyMap<
|
|
423
438
|
string,
|
|
@@ -436,7 +451,13 @@ export class UserSettingsBackendContribution {
|
|
|
436
451
|
packageId: pkg.packageId,
|
|
437
452
|
version: pkg.version,
|
|
438
453
|
...(pkg.dependencies ? { dependencies: pkg.dependencies } : {}),
|
|
454
|
+
...(pkg.platformOwned ? { platformOwned: true } : {}),
|
|
439
455
|
}));
|
|
456
|
+
this.platformOwnedPackageIds = new Set(
|
|
457
|
+
host.availablePackages
|
|
458
|
+
.filter((pkg) => pkg.platformOwned)
|
|
459
|
+
.map((pkg) => pkg.packageId),
|
|
460
|
+
);
|
|
440
461
|
this.availablePackages = new Set(
|
|
441
462
|
host.availablePackages.map(
|
|
442
463
|
({ packageId, version }) => `${packageId}\u0000${version}`,
|
|
@@ -455,7 +476,7 @@ export class UserSettingsBackendContribution {
|
|
|
455
476
|
]),
|
|
456
477
|
);
|
|
457
478
|
this.defaultPackages = host.availablePackages.flatMap((pkg) =>
|
|
458
|
-
pkg.installByDefault
|
|
479
|
+
pkg.installByDefault || pkg.platformOwned
|
|
459
480
|
? [
|
|
460
481
|
{
|
|
461
482
|
packageId: pkg.packageId,
|
|
@@ -489,6 +510,80 @@ export class UserSettingsBackendContribution {
|
|
|
489
510
|
this.enablementRolloutPackages = this.defaultPackages.filter((pkg) =>
|
|
490
511
|
rolloutPackageIds.has(pkg.packageId),
|
|
491
512
|
);
|
|
513
|
+
this.defaultEnabledPackages = new Map(
|
|
514
|
+
this.defaultPackages
|
|
515
|
+
.filter((pkg) => pkg.state === "installed")
|
|
516
|
+
.map((pkg) => [pkg.packageId, pkg]),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private addMissingPackages(
|
|
521
|
+
current: UserSettingsViewV1,
|
|
522
|
+
defaults: readonly PackageInstallationView[],
|
|
523
|
+
): UserSettingsViewV1 {
|
|
524
|
+
const installedPackageIds = new Set(
|
|
525
|
+
current.packages.map((pkg) => pkg.packageId),
|
|
526
|
+
);
|
|
527
|
+
const additions = defaults.filter(
|
|
528
|
+
(pkg) => !installedPackageIds.has(pkg.packageId),
|
|
529
|
+
);
|
|
530
|
+
return additions.length === 0
|
|
531
|
+
? current
|
|
532
|
+
: {
|
|
533
|
+
...current,
|
|
534
|
+
packages: [
|
|
535
|
+
...current.packages,
|
|
536
|
+
...additions.map((pkg) => structuredClone(pkg)),
|
|
537
|
+
],
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Apply the default-disabled choices first introduced by marker v2. */
|
|
542
|
+
private applyEnablementRolloutDefaults(
|
|
543
|
+
current: UserSettingsViewV1,
|
|
544
|
+
): UserSettingsViewV1 {
|
|
545
|
+
let changed = false;
|
|
546
|
+
const rolloutDefaults = new Map(
|
|
547
|
+
this.enablementRolloutPackages.map((pkg) => [pkg.packageId, pkg]),
|
|
548
|
+
);
|
|
549
|
+
const packages = current.packages.map((pkg) => {
|
|
550
|
+
const defaultPackage = rolloutDefaults.get(pkg.packageId);
|
|
551
|
+
if (
|
|
552
|
+
!defaultPackage ||
|
|
553
|
+
defaultPackage.version !== pkg.version ||
|
|
554
|
+
defaultPackage.state !== "disabled" ||
|
|
555
|
+
pkg.state !== "installed" ||
|
|
556
|
+
pkg.failure !== undefined ||
|
|
557
|
+
pkg.provenance === "catalog"
|
|
558
|
+
) {
|
|
559
|
+
return pkg;
|
|
560
|
+
}
|
|
561
|
+
changed = true;
|
|
562
|
+
return { ...pkg, state: "disabled" as const };
|
|
563
|
+
});
|
|
564
|
+
return changed ? { ...current, packages } : current;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** One-shot repair for rows the v0.2.3 marker-v2 rollout disabled. */
|
|
568
|
+
private repairEnablementRolloutV2(
|
|
569
|
+
current: UserSettingsViewV1,
|
|
570
|
+
): UserSettingsViewV1 {
|
|
571
|
+
let changed = false;
|
|
572
|
+
const packages = current.packages.map((pkg) => {
|
|
573
|
+
const defaultPackage = this.defaultEnabledPackages.get(pkg.packageId);
|
|
574
|
+
if (
|
|
575
|
+
!defaultPackage ||
|
|
576
|
+
defaultPackage.version !== pkg.version ||
|
|
577
|
+
pkg.state !== "disabled" ||
|
|
578
|
+
pkg.failure !== undefined ||
|
|
579
|
+
pkg.provenance === "catalog"
|
|
580
|
+
) {
|
|
581
|
+
return pkg;
|
|
582
|
+
}
|
|
583
|
+
changed = true;
|
|
584
|
+
return { ...pkg, state: "installed" as const };
|
|
585
|
+
});
|
|
586
|
+
return changed ? { ...current, packages } : current;
|
|
492
587
|
}
|
|
493
588
|
|
|
494
589
|
/**
|
|
@@ -511,70 +606,82 @@ export class UserSettingsBackendContribution {
|
|
|
511
606
|
const marker = await transaction.get<unknown>(
|
|
512
607
|
DEFAULT_PACKAGES_BOOTSTRAP_KEY,
|
|
513
608
|
);
|
|
609
|
+
const stored = await transaction.get<unknown>(STATE_KEY);
|
|
514
610
|
if (marker !== undefined) {
|
|
515
611
|
if (
|
|
516
612
|
!marker ||
|
|
517
613
|
typeof marker !== "object" ||
|
|
518
614
|
Array.isArray(marker) ||
|
|
519
615
|
Object.keys(marker).length !== 1 ||
|
|
520
|
-
|
|
521
|
-
(marker as { schemaVersion?: unknown }).schemaVersion
|
|
522
|
-
|
|
616
|
+
![1, 2, DEFAULT_PACKAGES_BOOTSTRAP_VERSION].includes(
|
|
617
|
+
(marker as { schemaVersion?: unknown }).schemaVersion as number,
|
|
618
|
+
)
|
|
523
619
|
) {
|
|
524
620
|
throw new Error("Stored default Package bootstrap is invalid");
|
|
525
621
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
622
|
+
const markerVersion = (marker as { schemaVersion: number })
|
|
623
|
+
.schemaVersion;
|
|
624
|
+
|
|
625
|
+
// Marker v1 predates default-disabled model Packages and their
|
|
626
|
+
// dependency closure. Seed that closure before the catalog-relative
|
|
627
|
+
// fixed point sees it, then validate the complete graph. A later
|
|
628
|
+
// marker gets only the platform-owned repair on this read: retirement
|
|
629
|
+
// and dependency validation are one-shot migration steps, never a
|
|
630
|
+
// read-time rule.
|
|
631
|
+
const rawMigrated =
|
|
534
632
|
stored === undefined
|
|
535
633
|
? initialState()
|
|
536
|
-
:
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
),
|
|
634
|
+
: migrateStoredUserSettingsV1(
|
|
635
|
+
stored,
|
|
636
|
+
markerVersion === 1 ? undefined : this.storedSettingsPackages,
|
|
637
|
+
"repair",
|
|
541
638
|
);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
639
|
+
let settingsChanged = stored !== undefined && rawMigrated !== stored;
|
|
640
|
+
let migrated = decodeUserSettingsViewV1(rawMigrated);
|
|
641
|
+
if (markerVersion === 1) {
|
|
642
|
+
const seeded = this.applyEnablementRolloutDefaults(
|
|
643
|
+
this.addMissingPackages(migrated, this.enablementRolloutPackages),
|
|
644
|
+
);
|
|
645
|
+
const validated = migrateStoredUserSettingsV1(
|
|
646
|
+
seeded,
|
|
647
|
+
this.storedSettingsPackages,
|
|
648
|
+
);
|
|
649
|
+
settingsChanged ||= seeded !== migrated || validated !== seeded;
|
|
650
|
+
migrated = decodeUserSettingsViewV1(validated);
|
|
651
|
+
}
|
|
652
|
+
if (markerVersion === 2) {
|
|
653
|
+
const repaired = this.repairEnablementRolloutV2(migrated);
|
|
654
|
+
settingsChanged ||= repaired !== migrated;
|
|
655
|
+
migrated = repaired;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (
|
|
659
|
+
markerVersion === DEFAULT_PACKAGES_BOOTSTRAP_VERSION &&
|
|
660
|
+
!settingsChanged
|
|
661
|
+
) {
|
|
662
|
+
return structuredClone(migrated);
|
|
663
|
+
}
|
|
664
|
+
const next = settingsChanged
|
|
665
|
+
? { ...migrated, revision: migrated.revision + 1 }
|
|
666
|
+
: migrated;
|
|
556
667
|
await transaction.put({
|
|
557
|
-
[STATE_KEY]: next,
|
|
668
|
+
...(settingsChanged ? { [STATE_KEY]: next } : {}),
|
|
558
669
|
[DEFAULT_PACKAGES_BOOTSTRAP_KEY]: {
|
|
559
670
|
schemaVersion: DEFAULT_PACKAGES_BOOTSTRAP_VERSION,
|
|
560
671
|
},
|
|
561
672
|
});
|
|
562
673
|
return structuredClone(next);
|
|
563
674
|
}
|
|
564
|
-
const current =
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
);
|
|
675
|
+
const current =
|
|
676
|
+
stored === undefined
|
|
677
|
+
? initialState()
|
|
678
|
+
: decodeUserSettingsViewV1(
|
|
679
|
+
migrateStoredUserSettingsV1(stored, this.storedSettingsPackages),
|
|
680
|
+
);
|
|
681
|
+
const seeded = this.addMissingPackages(current, this.defaultPackages);
|
|
571
682
|
const next = {
|
|
572
|
-
...
|
|
573
|
-
revision:
|
|
574
|
-
packages: [
|
|
575
|
-
...current.packages,
|
|
576
|
-
...additions.map((pkg) => structuredClone(pkg)),
|
|
577
|
-
],
|
|
683
|
+
...seeded,
|
|
684
|
+
revision: seeded.revision + 1,
|
|
578
685
|
} satisfies UserSettingsViewV1;
|
|
579
686
|
await transaction.put({
|
|
580
687
|
[STATE_KEY]: next,
|
|
@@ -625,6 +732,10 @@ export class UserSettingsBackendContribution {
|
|
|
625
732
|
|
|
626
733
|
async readConfiguration(input: unknown): Promise<UserSettingsViewV1> {
|
|
627
734
|
const request = decodeUserConfigurationReadRpcV1(input);
|
|
735
|
+
// Settings owns the stored-record migration and platform-row repair. Run
|
|
736
|
+
// it before Package bootstraps so every bootstrap observes the repaired
|
|
737
|
+
// current Catalog in the same configuration read.
|
|
738
|
+
await this.read(request.userId);
|
|
628
739
|
for (const bootstrap of this.readBootstraps.values()) {
|
|
629
740
|
await bootstrap.bootstrap(request.userId);
|
|
630
741
|
}
|
|
@@ -844,6 +955,21 @@ export class UserSettingsBackendContribution {
|
|
|
844
955
|
if (command.expectedRevision !== current.revision) {
|
|
845
956
|
throw new ConfigurationConflictError(current.revision);
|
|
846
957
|
}
|
|
958
|
+
if (
|
|
959
|
+
(command.type === "user/uninstall-package" ||
|
|
960
|
+
(command.type === "user/set-package-enabled" && !command.enabled)) &&
|
|
961
|
+
this.platformOwnedPackageIds.has(command.packageId)
|
|
962
|
+
) {
|
|
963
|
+
const receipt: OperationReceiptV1 = {
|
|
964
|
+
schemaVersion: 1,
|
|
965
|
+
commandId: command.commandId,
|
|
966
|
+
revision: current.revision,
|
|
967
|
+
status: "rejected",
|
|
968
|
+
failure: `Platform-owned Package "${command.packageId}" cannot be disabled or uninstalled`,
|
|
969
|
+
};
|
|
970
|
+
await storage.put(receiptKey, { commandFingerprint, receipt });
|
|
971
|
+
return receipt;
|
|
972
|
+
}
|
|
847
973
|
let dependencyFailure: string | undefined;
|
|
848
974
|
if (command.type === "user/install-package" && command.enabled !== false) {
|
|
849
975
|
dependencyFailure = this.packageDependencyFailure(
|