@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.
Files changed (79) hide show
  1. package/dist/admin/admin.schemas.d.ts +3 -0
  2. package/dist/catalog/catalog-content.d.ts +33 -1
  3. package/dist/catalog/catalog-content.js +65 -2
  4. package/dist/catalog/catalog-items.js +1 -0
  5. package/dist/catalog/catalog-scope.service.d.ts +29 -3
  6. package/dist/catalog/catalog-scope.service.js +82 -10
  7. package/dist/catalog/catalog.service.d.ts +7 -5
  8. package/dist/catalog/catalog.service.js +26 -5
  9. package/dist/catalog/claude/translate.js +3 -0
  10. package/dist/catalog/component-scope.schemas.d.ts +4 -0
  11. package/dist/catalog/component-scope.service.d.ts +48 -9
  12. package/dist/catalog/component-scope.service.js +152 -27
  13. package/dist/catalog/component-state.d.ts +12 -3
  14. package/dist/catalog/component-state.js +21 -3
  15. package/dist/catalog/marketplaces.service.d.ts +1 -1
  16. package/dist/catalog/org-catalog.service.js +3 -0
  17. package/dist/catalog/owned-items.js +7 -0
  18. package/dist/catalog/platform-components.d.ts +23 -3
  19. package/dist/catalog/platform-components.js +118 -9
  20. package/dist/catalog/platform-migration.js +2 -1
  21. package/dist/catalog/plugin-import.d.ts +24 -4
  22. package/dist/catalog/plugin-import.js +37 -6
  23. package/dist/catalog/plugin-install.d.ts +20 -1
  24. package/dist/catalog/plugin-install.js +71 -1
  25. package/dist/catalog/plugin-package.d.ts +41 -4
  26. package/dist/catalog/plugin-package.js +127 -7
  27. package/dist/catalog/plugin-upgrade.d.ts +13 -2
  28. package/dist/catalog/plugin-upgrade.js +124 -6
  29. package/dist/catalog/unshare.js +1 -0
  30. package/dist/db/kernel-schema.d.ts +41 -4
  31. package/dist/errors.d.ts +25 -0
  32. package/dist/errors.js +25 -0
  33. package/dist/http/api.module.js +7 -0
  34. package/dist/identity/accounts.d.ts +11 -4
  35. package/dist/identity/accounts.js +23 -10
  36. package/dist/identity/me.controller.d.ts +10 -10
  37. package/dist/identity/me.controller.js +8 -6
  38. package/dist/identity/me.schemas.d.ts +13 -3
  39. package/dist/identity/me.schemas.js +10 -2
  40. package/dist/identity/settings.d.ts +27 -0
  41. package/dist/identity/settings.js +64 -0
  42. package/dist/orgs/settings.service.d.ts +31 -0
  43. package/dist/orgs/settings.service.js +31 -1
  44. package/dist/platform/exclusions.js +16 -0
  45. package/dist/platform/operation.js +6 -2
  46. package/dist/platform/ops/admin.ops.d.ts +3 -1
  47. package/dist/platform/ops/admin.ops.js +25 -7
  48. package/dist/platform/ops/components.ops.js +1 -0
  49. package/dist/platform/ops/org-settings.ops.d.ts +3 -1
  50. package/dist/platform/ops/org-settings.ops.js +33 -8
  51. package/dist/platform/ops/orgs.ops.js +1 -0
  52. package/dist/platform/ops/platform.ops.d.ts +3 -1
  53. package/dist/platform/ops/platform.ops.js +43 -16
  54. package/dist/platform/ops/themes.ops.d.ts +14 -0
  55. package/dist/platform/ops/themes.ops.js +47 -0
  56. package/dist/platform/platform-ops.module.d.ts +2 -1
  57. package/dist/platform/platform-ops.module.js +5 -0
  58. package/dist/platform/route-schemas.js +3 -0
  59. package/dist/themes/render-css.d.ts +12 -0
  60. package/dist/themes/render-css.js +132 -0
  61. package/dist/themes/theme-delivery.service.d.ts +34 -0
  62. package/dist/themes/theme-delivery.service.js +111 -0
  63. package/dist/themes/theme-state.d.ts +95 -0
  64. package/dist/themes/theme-state.js +203 -0
  65. package/dist/themes/theme-usage.d.ts +61 -0
  66. package/dist/themes/theme-usage.js +98 -0
  67. package/dist/themes/themes-public.controller.d.ts +28 -0
  68. package/dist/themes/themes-public.controller.js +92 -0
  69. package/dist/themes/themes.controller.d.ts +13 -0
  70. package/dist/themes/themes.controller.js +38 -0
  71. package/dist/themes/themes.module.d.ts +3 -0
  72. package/dist/themes/themes.module.js +21 -0
  73. package/dist/themes/themes.schemas.d.ts +3 -0
  74. package/dist/themes/themes.schemas.js +2 -0
  75. package/dist/themes/themes.service.d.ts +50 -0
  76. package/dist/themes/themes.service.js +99 -0
  77. package/package.json +3 -3
  78. package/src/migrations/0102_themes.sql +64 -0
  79. package/src/migrations/0103_ui_theme_setting.sql +7 -0
@@ -29,6 +29,7 @@ import { KERNEL_LOGGER } from "../infra/tokens.js";
29
29
  import { OrgLock } from "../orgs/org-lock.js";
30
30
  import { MAINTENANCE_QUEUE_NAME } from "../runtime/maintenance-queue.js";
31
31
  import { SkillsService } from "../skills/skills.service.js";
32
+ import { assertNotPlatformDefault } from "../themes/theme-usage.js";
32
33
  import { CatalogService } from "./catalog.service.js";
33
34
  import { CatalogScopeService } from "./catalog-scope.service.js";
34
35
  import { notOwnedError } from "./component-promotion.js";
@@ -47,11 +48,13 @@ const COMPONENT_TABLES = [
47
48
  "platform_skills",
48
49
  "platform_agent_templates",
49
50
  "platform_connector_templates",
51
+ "platform_themes",
50
52
  ];
51
53
  const TABLE_OF = {
52
54
  skill: "platform_skills",
53
55
  agent_template: "platform_agent_templates",
54
56
  connector_template: "platform_connector_templates",
57
+ theme: "platform_themes",
55
58
  };
56
59
  /** The hash of a delete preview: sha256 over its outcome and sorted ids. */
57
60
  function previewHashOf(p) {
@@ -404,6 +407,10 @@ let OwnedItems = class OwnedItems {
404
407
  afterCommit(() => this.enqueueCleanup(item.id));
405
408
  return "removed";
406
409
  }
410
+ // A transfer leaves none of its themes `all` (an `all` scope is narrowed, P3-39): refused
411
+ // while one is the platform default (themes spec §7), before anything changes
412
+ // (its theme rows FOR UPDATE first: the theme lock order is item, theme row, `uiTheme`)
413
+ await assertNotPlatformDefault(t, (await loadComponents(t, "theme", { itemId: item.id, forUpdate: true })).map((c) => c.id), () => "narrower");
407
414
  // Read before anything changes: where each own-scope `all` component is on (P3-39)
408
415
  const narrow = [];
409
416
  for (const kind of COMPONENT_KINDS) {
@@ -1,4 +1,4 @@
1
- import { type AgentTemplate, type ConnectorTemplateFile } from "@capybara-hq/plugin-sdk";
1
+ import { type AgentTemplate, type ConnectorTemplateFile, type ParsedTheme } from "@capybara-hq/plugin-sdk";
2
2
  import type { SkillManifest, SourceFormat } from "../db/kernel-schema.js";
3
3
  import { KernelDb } from "../infra/database/kernel-db.js";
4
4
  import { TransactionHost } from "../infra/database/transaction-host.js";
@@ -6,9 +6,9 @@ import type { KernelLogger } from "../log.js";
6
6
  import { type SkillPackageFile } from "../skills/package.js";
7
7
  import { type PrefixedStorage, StorageService } from "../storage/storage.service.js";
8
8
  import { CatalogItemLock } from "./locks.js";
9
- /** A skill or agent template version a sync wrote (`from` null: the component was created). */
9
+ /** A skill, agent template or theme version a sync wrote (`from` null: the component was created). */
10
10
  export interface ChangedRef {
11
- kind: "skill" | "agent_template";
11
+ kind: "skill" | "agent_template" | "theme";
12
12
  id: string;
13
13
  name: string;
14
14
  from: number | null;
@@ -31,11 +31,17 @@ export interface PlatformSyncResult {
31
31
  skills: ChangedRef[];
32
32
  agentTemplates: ChangedRef[];
33
33
  connectorTemplates: ChangedConnectorTemplateRef[];
34
+ themes: ChangedRef[];
34
35
  }
35
36
  /** The prefix (in `deployment/`) of a platform skill version's files. */
36
37
  export declare const platformSkillPrefix: (id: string, version: number) => string;
37
38
  /** The prefix (in `deployment/`) of a platform agent template version's file. */
38
39
  export declare const platformAgentTemplatePrefix: (id: string, version: number) => string;
40
+ /**
41
+ * The prefix (in `deployment/`) of a platform theme's files (themes spec §6); a version's fonts
42
+ * and logo are under `<prefix><version>-<catalog version>/` ({@link syncVersionPrefix}).
43
+ */
44
+ export declare const platformThemePrefix: (id: string) => string;
39
45
  /**
40
46
  * A fresh prefix (in `deployment/`) for a version written before its transaction (ruling P3-45):
41
47
  * `<id>/<version>-<uuid>/`, so two writers racing for one version number never overwrite each
@@ -73,6 +79,19 @@ interface TemplateStep {
73
79
  file: SkillPackageFile;
74
80
  template: AgentTemplate;
75
81
  }
82
+ interface ThemeStep {
83
+ /** The theme id. */
84
+ name: string;
85
+ base: Base<number> | undefined;
86
+ /** The platform row id. */
87
+ id: string;
88
+ to: number;
89
+ /** null: the theme has no assets. */
90
+ prefix: string | null;
91
+ /** Fonts and logo, sanitized (paths relative to the plugin root). */
92
+ files: SkillPackageFile[];
93
+ theme: ParsedTheme;
94
+ }
76
95
  interface ConnectorTemplateStep {
77
96
  name: string;
78
97
  base: Base<string> | undefined;
@@ -88,6 +107,7 @@ export interface SyncPlan {
88
107
  skills: SkillStep[];
89
108
  agentTemplates: TemplateStep[];
90
109
  connectorTemplates: ConnectorTemplateStep[];
110
+ themes: ThemeStep[];
91
111
  }
92
112
  /** A component changed between the plan and its write (a concurrent sync): plan again. */
93
113
  export declare class PlanStale extends Error {
@@ -7,10 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
- // Platform components (plugin scope phase 2, rulings A2 and C2): the skills, agent templates and
11
- // connector templates of a catalog item, stored once for the deployment. Files live under
12
- // `deployment/platform/{skills,agent-templates}/<id>/<version>/`, never under an org prefix (orgs
13
- // are purged by prefix).
10
+ // Platform components (plugin scope phase 2, rulings A2 and C2): the skills, agent templates,
11
+ // connector templates and themes of a catalog item, stored once for the deployment. Files live
12
+ // under `deployment/platform/{skills,agent-templates,themes}/<id>/<version>…/`, never under an org
13
+ // prefix (orgs are purged by prefix).
14
14
  import { createHash, randomUUID } from "node:crypto";
15
15
  import { parseAgentTemplate, } from "@capybara-hq/plugin-sdk";
16
16
  import { Inject, Injectable } from "@nestjs/common";
@@ -24,7 +24,7 @@ import { packagePath } from "../packages/tgz.js";
24
24
  import { readSkillPackage, skillManifest } from "../skills/package.js";
25
25
  import { StorageService } from "../storage/storage.service.js";
26
26
  import { canonicalJson } from "../tools/canonical-json.js";
27
- import { readCatalogAgentTemplates, readCatalogFiles, readCatalogTemplates, skillFilesDigest, skillOf, } from "./catalog-content.js";
27
+ import { readCatalogAgentTemplates, readCatalogFiles, readCatalogTemplates, readCatalogThemes, skillFilesDigest, skillOf, themeAssetsDigest, } from "./catalog-content.js";
28
28
  import { putCatalogFiles } from "./catalog-items.js";
29
29
  import { CatalogItemLock } from "./locks.js";
30
30
  import { assertNotAwaiting } from "./ownership.js";
@@ -32,6 +32,11 @@ import { assertNotAwaiting } from "./ownership.js";
32
32
  export const platformSkillPrefix = (id, version) => `platform/skills/${id}/${version}/`;
33
33
  /** The prefix (in `deployment/`) of a platform agent template version's file. */
34
34
  export const platformAgentTemplatePrefix = (id, version) => `platform/agent-templates/${id}/${version}/`;
35
+ /**
36
+ * The prefix (in `deployment/`) of a platform theme's files (themes spec §6); a version's fonts
37
+ * and logo are under `<prefix><version>-<catalog version>/` ({@link syncVersionPrefix}).
38
+ */
39
+ export const platformThemePrefix = (id) => `platform/themes/${id}/`;
35
40
  /**
36
41
  * A fresh prefix (in `deployment/`) for a version written before its transaction (ruling P3-45):
37
42
  * `<id>/<version>-<uuid>/`, so two writers racing for one version number never overwrite each
@@ -94,9 +99,10 @@ let PlatformComponents = class PlatformComponents {
94
99
  const plan = await this.plan(itemId, version);
95
100
  if (plan.skills.length === 0 &&
96
101
  plan.agentTemplates.length === 0 &&
97
- plan.connectorTemplates.length === 0) {
102
+ plan.connectorTemplates.length === 0 &&
103
+ plan.themes.length === 0) {
98
104
  await this.markSynced(itemId, version);
99
- return { skills: [], agentTemplates: [], connectorTemplates: [] };
105
+ return { skills: [], agentTemplates: [], connectorTemplates: [], themes: [] };
100
106
  }
101
107
  await this.writeFiles(plan);
102
108
  try {
@@ -109,6 +115,7 @@ let PlatformComponents = class PlatformComponents {
109
115
  skills: r.skills.length,
110
116
  agentTemplates: r.agentTemplates.length,
111
117
  connectorTemplates: r.connectorTemplates.length,
118
+ themes: r.themes.length,
112
119
  });
113
120
  return r;
114
121
  }
@@ -449,6 +456,7 @@ let PlatformComponents = class PlatformComponents {
449
456
  skills: [],
450
457
  agentTemplates: [],
451
458
  connectorTemplates: [],
459
+ themes: [],
452
460
  };
453
461
  for (const s of manifest.skills ?? []) {
454
462
  const files = await readCatalogFiles(this.files, row.object_prefix, row.files, packagePath(s.path));
@@ -524,17 +532,53 @@ let PlatformComponents = class PlatformComponents {
524
532
  file: t.file,
525
533
  });
526
534
  }
535
+ // Themes (by plugin and id): changed when the definition or an asset changed
536
+ for (const t of await readCatalogThemes(this.files, v)) {
537
+ const live = await this.db
538
+ .selectFrom("platform_themes as p")
539
+ .innerJoin("platform_theme_versions as pv", (j) => j.onRef("pv.theme_id", "=", "p.id").onRef("pv.version", "=", "p.current_version"))
540
+ .leftJoin("catalog_item_versions as cv", (j) => j
541
+ .onRef("cv.item_id", "=", "p.catalog_item_id")
542
+ .onRef("cv.version", "=", "pv.catalog_version"))
543
+ .select(["p.id", "p.current_version", "pv.definition", "cv.files"])
544
+ .where("p.plugin_name", "=", plugin)
545
+ .where("p.name", "=", t.id)
546
+ .where("p.deleted_at", "is", null)
547
+ .executeTakeFirst();
548
+ if (live &&
549
+ canonicalJson(live.definition) === canonicalJson(t.theme) &&
550
+ live.files !== null &&
551
+ themeAssetsDigest(live.files, live.definition) === t.assetsDigest) {
552
+ continue;
553
+ }
554
+ const id = live?.id ?? randomUUID();
555
+ const to = (live?.current_version ?? 0) + 1;
556
+ plan.themes.push({
557
+ name: t.id,
558
+ base: live && { id: live.id, version: live.current_version },
559
+ id,
560
+ to,
561
+ prefix: t.assets.length > 0
562
+ ? syncVersionPrefix(`${platformThemePrefix(id)}${to}/`, version)
563
+ : null,
564
+ files: t.assets,
565
+ theme: t.theme,
566
+ });
567
+ }
527
568
  return plan;
528
569
  }
529
570
  /** Copies the planned versions' files to their platform prefixes. */
530
571
  async writeFiles(plan) {
531
- if (plan.skills.length === 0 && plan.agentTemplates.length === 0)
572
+ const themes = plan.themes.filter((t) => t.prefix !== null);
573
+ if (plan.skills.length === 0 && plan.agentTemplates.length === 0 && themes.length === 0)
532
574
  return;
533
575
  this.files.assertAvailable();
534
576
  for (const s of plan.skills)
535
577
  await putCatalogFiles(this.files, s.prefix, s.files);
536
578
  for (const a of plan.agentTemplates)
537
579
  await putCatalogFiles(this.files, a.prefix, [a.file]);
580
+ for (const t of themes)
581
+ await putCatalogFiles(this.files, t.prefix, t.files);
538
582
  }
539
583
  /**
540
584
  * Writes the planned rows under the item name's lock (the lock of the import confirm), after
@@ -543,7 +587,12 @@ let PlatformComponents = class PlatformComponents {
543
587
  async apply(plan) {
544
588
  await this.itemLock.lock(plan.plugin);
545
589
  const t = this.db;
546
- const out = { skills: [], agentTemplates: [], connectorTemplates: [] };
590
+ const out = {
591
+ skills: [],
592
+ agentTemplates: [],
593
+ connectorTemplates: [],
594
+ themes: [],
595
+ };
547
596
  // New component rows carry the plugin's owner and 可共享 flag (ruling P3-2); an org plugin's
548
597
  // skills and agent templates were created in its owner org (`origin_org_id`, immutable, F27),
549
598
  // and like the plugin they are not shown to other orgs until the super admin shows them
@@ -725,6 +774,66 @@ let PlatformComponents = class PlatformComponents {
725
774
  to: plan.version,
726
775
  });
727
776
  }
777
+ for (const th of plan.themes) {
778
+ const live = await t
779
+ .selectFrom("platform_themes")
780
+ .select(["id", "current_version"])
781
+ .where("plugin_name", "=", plan.plugin)
782
+ .where("name", "=", th.name)
783
+ .where("deleted_at", "is", null)
784
+ .forUpdate()
785
+ .executeTakeFirst();
786
+ assertBase(live && { id: live.id, version: live.current_version }, th.base);
787
+ const description = th.theme.description?.en ?? null;
788
+ const descriptionZh = th.theme.description?.zh ?? null;
789
+ if (live) {
790
+ await t
791
+ .updateTable("platform_themes")
792
+ .set({
793
+ current_version: th.to,
794
+ description,
795
+ description_zh: descriptionZh,
796
+ catalog_item_id: plan.itemId,
797
+ updated_at: new Date(),
798
+ })
799
+ .where("id", "=", th.id)
800
+ .execute();
801
+ }
802
+ else {
803
+ await t
804
+ .insertInto("platform_themes")
805
+ .values({
806
+ id: th.id,
807
+ catalog_item_id: plan.itemId,
808
+ plugin_name: plan.plugin,
809
+ name: th.name,
810
+ description,
811
+ description_zh: descriptionZh,
812
+ current_version: th.to,
813
+ scope_mode: "follow",
814
+ ...owned,
815
+ ...origin,
816
+ })
817
+ .execute();
818
+ }
819
+ await t
820
+ .insertInto("platform_theme_versions")
821
+ .values({
822
+ theme_id: th.id,
823
+ version: th.to,
824
+ definition: JSON.stringify(th.theme),
825
+ object_prefix: th.prefix,
826
+ catalog_version: plan.version,
827
+ })
828
+ .execute();
829
+ out.themes.push({
830
+ kind: "theme",
831
+ id: th.id,
832
+ name: th.name,
833
+ from: th.base?.version ?? null,
834
+ to: th.to,
835
+ });
836
+ }
728
837
  return out;
729
838
  }
730
839
  };
@@ -203,7 +203,8 @@ let PlatformMigration = class PlatformMigration {
203
203
  const version = await this.platformVersion(item.id, item.latest_version);
204
204
  try {
205
205
  const r = await this.platform.syncFromVersion(item.id, version, { initial: false });
206
- if (r.skills.length + r.agentTemplates.length + r.connectorTemplates.length > 0) {
206
+ const changed = r.skills.length + r.agentTemplates.length + r.connectorTemplates.length + r.themes.length;
207
+ if (changed > 0) {
207
208
  stats.itemsSynced.push(`${item.name}@${version}`);
208
209
  }
209
210
  }
@@ -1,4 +1,4 @@
1
- import { type AgentTemplate, type AgentTemplateTier, type LocalizedName, type PluginManifest } from "@capybara-hq/plugin-sdk";
1
+ import { type AgentTemplate, type AgentTemplateTier, type LocalizedName, type PluginManifest, type ThemeScheme } from "@capybara-hq/plugin-sdk";
2
2
  import type { Queue } from "bullmq";
3
3
  import { ApiError } from "../errors.js";
4
4
  import { ExtensionAdminOps } from "../extensions/admin-ops.js";
@@ -134,6 +134,18 @@ export interface PluginReviewAgentTemplate {
134
134
  maxSteps?: number;
135
135
  skills?: string[];
136
136
  }
137
+ /** One theme in a review (themes spec §6): what the super admin sees before confirming. */
138
+ export interface PluginReviewTheme {
139
+ /** The theme id (the file's base name). */
140
+ id: string;
141
+ name: LocalizedName;
142
+ /** The schemes it defines. */
143
+ schemes: ThemeScheme[];
144
+ /** Number of fonts. */
145
+ fonts: number;
146
+ /** It replaces the logo mark, the wordmark or the favicon. */
147
+ logo: boolean;
148
+ }
137
149
  /** The source of a staged import. */
138
150
  export interface PluginImportSource {
139
151
  /**
@@ -215,6 +227,11 @@ export interface PluginReview {
215
227
  * {@link pluginReviewHash} when empty, so their digests are unchanged.
216
228
  */
217
229
  agentTemplates?: PluginReviewAgentTemplate[];
230
+ /**
231
+ * Themes (themes spec §6). Absent without any; left out of {@link pluginReviewHash} when empty,
232
+ * so the digests of plugins without themes are unchanged.
233
+ */
234
+ themes?: PluginReviewTheme[];
218
235
  /**
219
236
  * Each dependency with the highest imported catalog version satisfying its range (null when
220
237
  * nothing does yet: the import still completes, an org install is refused until it is imported).
@@ -333,7 +350,9 @@ export interface StagedTarget {
333
350
  }
334
351
  /**
335
352
  * The API error of a plugin package that cannot be read in a catalog path: the pre-0.1.0 format ->
336
- * `catalog.legacy_format`, anything else -> `catalog.invalid_manifest` (issues as details).
353
+ * `catalog.legacy_format`; a failing theme (themes spec §5) -> `theme.invalid` (details
354
+ * `[{ theme, errors }]`) or `theme.unreadable` (details `[{ theme, issues }]`, the failing pairs);
355
+ * anything else -> `catalog.invalid_manifest` (issues as details).
337
356
  */
338
357
  export declare function catalogPackageError(e: PluginPackageError | ExtensionPackageError): ApiError;
339
358
  export declare class PluginImport {
@@ -629,10 +648,11 @@ export declare class PluginImport {
629
648
  * current version / package, its own review digest), every template field, each skill's name / path
630
649
  * / file count, the dependency resolution, the sha256 of every stored file, the format, the
631
650
  * translation report's items, the sha256 of the license text and the two review flags
632
- * (`claudeManifestIgnored`, `licenseTruncated`), and the agent templates when there are any.
651
+ * (`claudeManifestIgnored`, `licenseTruncated`), the agent templates when there are any and the
652
+ * themes when there are any.
633
653
  * Exported for tests. `repository` is display only.
634
654
  */
635
- export declare function pluginReviewHash(r: Pick<PluginReview, "name" | "version" | "contentDigest" | "extension" | "connectorTemplates" | "agentTemplates" | "skills" | "dependencies" | "format" | "translation" | "licenseText" | "claudeManifestIgnored" | "licenseTruncated">, files: {
655
+ export declare function pluginReviewHash(r: Pick<PluginReview, "name" | "version" | "contentDigest" | "extension" | "connectorTemplates" | "agentTemplates" | "themes" | "skills" | "dependencies" | "format" | "translation" | "licenseText" | "claudeManifestIgnored" | "licenseTruncated">, files: {
636
656
  path: string;
637
657
  sha256: string;
638
658
  }[]): string;
@@ -52,7 +52,7 @@ import { CatalogItemLock } from "./locks.js";
52
52
  import { assertNameFree, assertPackageFree, assertShareable, packageOwner, releaseExtensionClaim, shareGate, usableBy, } from "./ownership.js";
53
53
  import { PlatformComponents } from "./platform-components.js";
54
54
  import { PLATFORM_MIGRATION_JOB } from "./platform-migration.js";
55
- import { PluginPackageError, packagePath, readPluginFiles, readPluginPackage, } from "./plugin-package.js";
55
+ import { PluginPackageError, packagePath, readPluginFiles, readPluginPackage, themeApiError, } from "./plugin-package.js";
56
56
  import { hasImportRequests, importRequestOf, resolveImportRequests } from "./request-rules.js";
57
57
  import { assertNameNotReserved } from "./reserved-names.js";
58
58
  import { addScopeOrg } from "./scope-orgs.js";
@@ -112,12 +112,17 @@ const reviewStale = (review) => new ApiError("plugin.review_stale", `The review
112
112
  const mismatch = (what) => new ApiError("marketplace.package_mismatch", `The downloaded package does not match the marketplace entry: ${what}`);
113
113
  /**
114
114
  * The API error of a plugin package that cannot be read in a catalog path: the pre-0.1.0 format ->
115
- * `catalog.legacy_format`, anything else -> `catalog.invalid_manifest` (issues as details).
115
+ * `catalog.legacy_format`; a failing theme (themes spec §5) -> `theme.invalid` (details
116
+ * `[{ theme, errors }]`) or `theme.unreadable` (details `[{ theme, issues }]`, the failing pairs);
117
+ * anything else -> `catalog.invalid_manifest` (issues as details).
116
118
  */
117
119
  export function catalogPackageError(e) {
118
120
  const legacy = e.code === "catalog.legacy_format" || e.code === "extension.legacy_format";
119
121
  if (legacy)
120
122
  return new ApiError("catalog.legacy_format", e.message, e.issues);
123
+ if (e.code === "theme.invalid" || e.code === "theme.unreadable") {
124
+ return themeApiError(e);
125
+ }
121
126
  return invalid(e.issues);
122
127
  }
123
128
  /** Runs a package read, mapping its errors with {@link catalogPackageError}. */
@@ -380,7 +385,7 @@ let PluginImport = class PluginImport {
380
385
  const objectFiles = content.files.filter((f) => content.objectPaths.has(f.path));
381
386
  if (objectFiles.length > 0 && retry === undefined) {
382
387
  if (source.kind === "bootstrap" && !this.files.available) {
383
- throw new ApiError("storage.unavailable", `Bootstrap plugin ${m.name} ships skills, connector templates or agent templates, which are kept in object storage, and object storage is not running yet (it may be a bootstrap plugin itself); a required bootstrap plugin may ship only an icon and setup guides, an optional one is retried once the extensions have loaded`);
388
+ throw new ApiError("storage.unavailable", `Bootstrap plugin ${m.name} ships skills, connector templates, agent templates or themes, which are kept in object storage, and object storage is not running yet (it may be a bootstrap plugin itself); a required bootstrap plugin may ship only an icon and setup guides, an optional one is retried once the extensions have loaded`);
384
389
  }
385
390
  this.files.assertAvailable();
386
391
  }
@@ -430,6 +435,7 @@ let PluginImport = class PluginImport {
430
435
  skills: content.skills,
431
436
  connectorTemplates: content.templates,
432
437
  ...(content.agentTemplates.length > 0 && { agentTemplates: content.agentTemplates }),
438
+ ...(content.themes.length > 0 && { themes: content.themes }),
433
439
  dependencies,
434
440
  source: src,
435
441
  provenance: (opts.provenance ?? NOT_NPM).provenance,
@@ -1456,6 +1462,23 @@ let PluginImport = class PluginImport {
1456
1462
  const { name, description, instructions, model, ...rest } = a.template;
1457
1463
  return { name, path: a.path, description, instructions, model: model ?? "inherit", ...rest };
1458
1464
  });
1465
+ // Themes: the file as written, the assets as sanitized by the reader (never the uploaded
1466
+ // originals: a shared path, e.g. the plugin icon, is stored sanitized too)
1467
+ const themes = pkg.themes.map((t) => {
1468
+ keepObject(t.path);
1469
+ for (const [path, data] of t.assets) {
1470
+ stored.set(path, fileOf(path, data));
1471
+ objectPaths.add(path);
1472
+ }
1473
+ const logo = t.theme.logo;
1474
+ return {
1475
+ id: t.id,
1476
+ name: t.theme.name,
1477
+ schemes: ["dark", "light"].filter((k) => t.theme.schemes[k] !== undefined),
1478
+ fonts: t.theme.fonts?.length ?? 0,
1479
+ logo: logo !== undefined && Object.keys(logo).length > 0,
1480
+ };
1481
+ });
1459
1482
  if (problems.length > 0)
1460
1483
  throw invalid(problems);
1461
1484
  return {
@@ -1465,6 +1488,7 @@ let PluginImport = class PluginImport {
1465
1488
  templates,
1466
1489
  skills,
1467
1490
  agentTemplates,
1491
+ themes,
1468
1492
  notLoaded,
1469
1493
  templatePaths,
1470
1494
  };
@@ -1639,7 +1663,7 @@ export { PluginImport };
1639
1663
  */
1640
1664
  function normalizedManifest(pkg) {
1641
1665
  const pinned = new Map((pkg.manifest.skills ?? []).map((s) => [packagePath(s.path), s.pinned]));
1642
- const { agents: _, ...manifest } = pkg.manifest;
1666
+ const { agents: _, themes: __, ...manifest } = pkg.manifest;
1643
1667
  return {
1644
1668
  ...manifest,
1645
1669
  skills: pkg.skills.map((s) => {
@@ -1651,6 +1675,7 @@ function normalizedManifest(pkg) {
1651
1675
  ...(pkg.agentTemplates.length > 0 && {
1652
1676
  agents: pkg.agentTemplates.map((a) => ({ path: `./${a.path}` })),
1653
1677
  }),
1678
+ ...(pkg.themes.length > 0 && { themes: pkg.themes.map((t) => ({ path: `./${t.path}` })) }),
1654
1679
  };
1655
1680
  }
1656
1681
  /** package.json `name`, or null without one (a git plugin; never the string "undefined"). */
@@ -1682,7 +1707,8 @@ function actionOf(s, sha256, version) {
1682
1707
  * current version / package, its own review digest), every template field, each skill's name / path
1683
1708
  * / file count, the dependency resolution, the sha256 of every stored file, the format, the
1684
1709
  * translation report's items, the sha256 of the license text and the two review flags
1685
- * (`claudeManifestIgnored`, `licenseTruncated`), and the agent templates when there are any.
1710
+ * (`claudeManifestIgnored`, `licenseTruncated`), the agent templates when there are any and the
1711
+ * themes when there are any.
1686
1712
  * Exported for tests. `repository` is display only.
1687
1713
  */
1688
1714
  export function pluginReviewHash(r, files) {
@@ -1715,6 +1741,10 @@ export function pluginReviewHash(r, files) {
1715
1741
  ...((r.agentTemplates ?? []).length > 0
1716
1742
  ? [(r.agentTemplates ?? []).map((a) => [a.path, canonicalJson(a)])]
1717
1743
  : []),
1744
+ // themes, only when there are any, tagged (never read as agent templates)
1745
+ ...((r.themes ?? []).length > 0
1746
+ ? [{ themes: (r.themes ?? []).map((t) => [t.id, canonicalJson(t)]) }]
1747
+ : []),
1718
1748
  ]);
1719
1749
  return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
1720
1750
  }
@@ -1785,7 +1815,8 @@ function ownStaging(owner) {
1785
1815
  /**
1786
1816
  * An org upload's custom deny, by content (ruling P3-15'): skills or agent templates ->
1787
1817
  * `customSkills`, connector templates -> `customConnectors`; each `deny` -> 403
1788
- * `catalog.custom_disabled`. A package carrying neither passes.
1818
+ * `catalog.custom_disabled`. A package carrying neither passes; themes are covered by neither
1819
+ * switch (themes spec §6), so a themes-only upload passes.
1789
1820
  */
1790
1821
  async function assertCustomContent(db, content) {
1791
1822
  if (content.skills.length > 0 || content.agentTemplates.length > 0) {
@@ -11,6 +11,7 @@ import { OrgLock } from "../orgs/org-lock.js";
11
11
  import type { OrgAction } from "../orgs/roles.js";
12
12
  import { type SkillConflict, SkillsService } from "../skills/skills.service.js";
13
13
  import { StorageService } from "../storage/storage.service.js";
14
+ import { type ThemePreview } from "../themes/theme-usage.js";
14
15
  import type { ConnectorDetail, CreateConnectorInput, ToolIssue } from "../tools/connectors.service.js";
15
16
  import { ConnectorsService } from "../tools/connectors.service.js";
16
17
  import { type CatalogVersionFiles, type TemplateDeps } from "./catalog-content.js";
@@ -42,7 +43,7 @@ export interface PluginInstallInput {
42
43
  * agent template (ref = its agent_templates id; like a skill, `skill_version` records the version
43
44
  * an install or upgrade added to an existing template).
44
45
  */
45
- export type ComponentKind = "extension" | "connector" | "skill" | "plugin" | "agent_template";
46
+ export type ComponentKind = "extension" | "connector" | "skill" | "plugin" | "agent_template" | "theme";
46
47
  export interface UninstallReport {
47
48
  removed: {
48
49
  kind: ComponentKind;
@@ -84,6 +85,11 @@ export interface UninstallPreview {
84
85
  ref: string;
85
86
  }[];
86
87
  }[];
88
+ /**
89
+ * The plugin's themes on in the org through it (`follow`) that go off with it (themes spec §6):
90
+ * whether it is the org's default and how many people picked it. Absent when there are none.
91
+ */
92
+ themes?: ThemePreview[];
87
93
  }
88
94
  /** The confirm hash of an uninstall preview: sha256 over its sorted JSON (the scheme of `diffHash`). */
89
95
  export declare function uninstallPreviewHash(p: UninstallPreview): string;
@@ -303,6 +309,12 @@ export declare class PluginInstaller {
303
309
  * and a name two plugins of the same plan both bring (the one installed first is the source).
304
310
  */
305
311
  private assertNoSkillConflicts;
312
+ /**
313
+ * Refuses installing a plugin whose themes would come on in the org (they follow it) while
314
+ * another theme with the same id is on there (themes spec §6, rule 10): 409 `theme.id_taken`,
315
+ * details `[{ theme, orgId, themes, plugins }]`, before anything is written.
316
+ */
317
+ private assertNoThemeClashes;
306
318
  /** The connector checks of templates against what this replica loads right now. */
307
319
  templateDeps(): TemplateDeps;
308
320
  /**
@@ -453,6 +465,13 @@ export declare class PluginInstaller {
453
465
  * nothing written), and keeps the connectors it created as the org's own (`org_owned`).
454
466
  */
455
467
  private writeUninstall;
468
+ /**
469
+ * 409 `theme.platform_default` when uninstalling `installId` would switch the platform default
470
+ * theme off in the org (one of the plugin's `follow` themes on there through it, themes spec §7),
471
+ * in the caller's transaction before anything is written. A missing install passes (the
472
+ * uninstall reports it).
473
+ */
474
+ assertKeepsPlatformDefault(orgId: string, installId: string): Promise<void>;
456
475
  /**
457
476
  * The oldest other live install (not in `removing`) that has component `kind` / `ref`, or
458
477
  * undefined.