@uniqbit/mate-core 0.18.4 → 1.0.0-canary.0

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 (231) hide show
  1. package/claude-plugin/hooks/companion-instructions.mjs +9 -0
  2. package/claude-plugin/hooks/hooks.json +12 -2
  3. package/opencode-plugin/src/add-dir.ts +76 -10
  4. package/opencode-plugin/src/companion.ts +1 -8
  5. package/opencode-plugin/src/server.ts +2 -15
  6. package/package.json +10 -2
  7. package/src/brand/art.generated.ts +69 -0
  8. package/src/brand/index.ts +12 -0
  9. package/src/brand/palette.ts +19 -0
  10. package/src/brand/svg.generated.ts +8 -0
  11. package/src/brand/terminal.ts +83 -0
  12. package/src/brand/types.ts +33 -0
  13. package/src/cli/commands/artifact/artifact.ts +8 -0
  14. package/src/cli/commands/artifact/delivery/archive.ts +117 -0
  15. package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
  16. package/src/cli/commands/artifact/delivery/environment.ts +104 -0
  17. package/src/cli/commands/artifact/delivery/link.ts +154 -0
  18. package/src/cli/commands/artifact/delivery/output.ts +58 -0
  19. package/src/cli/commands/artifact/finish/command.ts +33 -3
  20. package/src/cli/commands/artifact/finish/engine.ts +47 -3
  21. package/src/cli/commands/artifact/finish/finisher.ts +10 -0
  22. package/src/cli/commands/artifact/finish/openspec.ts +200 -129
  23. package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
  24. package/src/cli/commands/artifact/pending/command.ts +31 -3
  25. package/src/cli/commands/artifact/pending/discovery.ts +21 -57
  26. package/src/cli/commands/cap/graphify.ts +52 -3
  27. package/src/cli/commands/cap/index-cmd.ts +8 -1
  28. package/src/cli/commands/cap/openspec.ts +13 -25
  29. package/src/cli/commands/companion/open.ts +56 -39
  30. package/src/cli/commands/companion/update.ts +75 -42
  31. package/src/cli/commands/doctor.ts +257 -78
  32. package/src/cli/commands/install.ts +0 -8
  33. package/src/cli/commands/plugin/install.ts +9 -4
  34. package/src/cli/commands/plugin/plugin.ts +3 -2
  35. package/src/cli/commands/report/adapter.ts +11 -2
  36. package/src/cli/commands/report/contract.ts +21 -0
  37. package/src/cli/commands/report/highlight.ts +34 -4
  38. package/src/cli/commands/report/markdown.ts +110 -0
  39. package/src/cli/commands/report/palette.ts +118 -0
  40. package/src/cli/commands/report/renderer.ts +456 -153
  41. package/src/cli/commands/report/types.ts +16 -0
  42. package/src/cli/commands/setup.ts +7 -15
  43. package/src/cli/commands/studio/client-asset.ts +16 -0
  44. package/src/cli/commands/studio/openspec-cli.ts +10 -58
  45. package/src/cli/commands/studio/routes.ts +2 -0
  46. package/src/cli/commands/studio/selection.ts +19 -1
  47. package/src/cli/commands/studio/server.ts +42 -1
  48. package/src/cli/commands/studio/terminal-launch.ts +7 -16
  49. package/src/cli/commands/studio/views/client.ts +6 -616
  50. package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
  51. package/src/cli/commands/studio/views/document.tsx +137 -69
  52. package/src/cli/commands/studio/views/markdown.tsx +250 -0
  53. package/src/cli/commands/studio/views/palette.ts +134 -0
  54. package/src/cli/commands/studio/views/reports/index.tsx +2 -7
  55. package/src/cli/commands/studio/views/skills/index.tsx +43 -62
  56. package/src/cli/commands/studio/views/specs/index.tsx +7 -15
  57. package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
  58. package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
  59. package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
  60. package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
  61. package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
  62. package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
  63. package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
  64. package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
  65. package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
  66. package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
  67. package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
  68. package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
  69. package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
  70. package/src/cli/commands/studio/views/styles.ts +217 -206
  71. package/src/cli/commands/studio/views/vault/state.ts +3 -5
  72. package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
  73. package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
  74. package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
  75. package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
  76. package/src/cli/commands/wrap.ts +9 -42
  77. package/src/cli/main.ts +4 -1
  78. package/src/cli/parse-flags.ts +25 -7
  79. package/src/cli/setup-selector.tsx +38 -162
  80. package/src/cli/usage.ts +5 -2
  81. package/src/hooks/companion-instructions.ts +190 -0
  82. package/src/hooks/hook-io.ts +63 -0
  83. package/src/hooks/session-banner.ts +21 -29
  84. package/src/hooks/session-guidance.ts +10 -36
  85. package/src/hooks/session-prepare.ts +115 -28
  86. package/src/hooks/validate-artifact-path.ts +82 -24
  87. package/src/index.ts +4 -0
  88. package/src/lib/change-layout.ts +154 -0
  89. package/src/lib/components/wizard-header.tsx +3 -1
  90. package/src/lib/context-mode-package.ts +24 -11
  91. package/src/lib/delivery/archive-change.ts +154 -0
  92. package/src/lib/delivery/branches.ts +108 -0
  93. package/src/lib/delivery/environment.ts +29 -0
  94. package/src/lib/delivery/forge.ts +279 -0
  95. package/src/lib/delivery/overlap.ts +202 -0
  96. package/src/lib/delivery/record-delivery.ts +196 -0
  97. package/src/lib/delivery/record.ts +343 -0
  98. package/src/lib/delivery/refresh.ts +174 -0
  99. package/src/lib/delivery/repository-access.ts +97 -0
  100. package/src/lib/delivery/stages.ts +111 -0
  101. package/src/lib/delivery/token.ts +58 -0
  102. package/src/lib/frontmatter.ts +18 -0
  103. package/src/lib/opencode-plugin-package.ts +5 -5
  104. package/src/lib/openspec-cli.ts +116 -0
  105. package/src/lib/openspec-scopes.ts +86 -0
  106. package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
  107. package/src/lib/orchestrator/adapters/claude.ts +60 -162
  108. package/src/lib/orchestrator/adapters/opencode.ts +9 -24
  109. package/src/lib/orchestrator/config-store.ts +2 -70
  110. package/src/lib/orchestrator/editor.ts +9 -9
  111. package/src/lib/orchestrator/openspec-store.ts +14 -54
  112. package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
  113. package/src/lib/orchestrator/projection-claude-links.ts +159 -0
  114. package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
  115. package/src/lib/orchestrator/projection-entries.ts +45 -15
  116. package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
  117. package/src/lib/orchestrator/projection-types.ts +3 -2
  118. package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
  119. package/src/lib/orchestrator/session-prepare.ts +49 -87
  120. package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
  121. package/src/lib/orchestrator/types.ts +2 -2
  122. package/src/lib/orchestrator/wrap-pass.ts +22 -88
  123. package/src/lib/package-paths.ts +8 -0
  124. package/src/opencode/companion-hooks.ts +64 -5
  125. package/src/opencode/companion-policy.ts +37 -18
  126. package/src/opencode/projected-guidance.ts +4 -1
  127. package/src/opencode/session-prepare.ts +2 -1
  128. package/src/opencode/tui.tsx +4 -2
  129. package/src/runtime/artifact-refusal.ts +63 -0
  130. package/src/runtime/claude-delivery-plan.ts +89 -7
  131. package/src/runtime/commit-attribution-guard.ts +181 -0
  132. package/src/runtime/companion-guidance.ts +15 -3
  133. package/src/runtime/companion-instructions.ts +340 -0
  134. package/src/runtime/error-message.ts +4 -0
  135. package/src/runtime/guidance.ts +0 -17
  136. package/src/runtime/index.ts +5 -0
  137. package/src/runtime/managed-block.ts +117 -0
  138. package/src/runtime/markdown-scan.ts +266 -0
  139. package/src/runtime/policy.ts +16 -15
  140. package/src/runtime/projected-flag.ts +7 -0
  141. package/src/runtime/projected-guidance.ts +1 -0
  142. package/src/runtime/runtime-document-manifest.ts +62 -28
  143. package/src/runtime/secret-guard.ts +148 -0
  144. package/src/runtime/secret-patterns.ts +164 -0
  145. package/src/runtime/secret-scan.ts +90 -0
  146. package/src/runtime/session-kind.ts +13 -0
  147. package/src/runtime/shell-heredoc.ts +81 -0
  148. package/src/runtime/wrap-state.ts +36 -6
  149. package/src/runtime/write-atomic.ts +64 -0
  150. package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
  151. package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
  152. package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
  153. package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
  154. package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
  155. package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
  156. package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
  157. package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
  158. package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
  159. package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
  160. package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
  161. package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
  162. package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
  163. package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
  164. package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
  165. package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
  166. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
  167. package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
  168. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  169. package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  170. package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
  171. package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
  172. package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
  173. package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
  174. package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
  175. package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
  176. package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
  177. package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
  178. package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
  179. package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
  180. package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
  181. package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
  182. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
  183. package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
  184. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
  185. package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
  186. package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
  187. package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
  188. package/src/templates/mate-skills/openspec-references.ts +140 -0
  189. package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
  190. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
  191. package/src/tools/setup/capabilities/context-mode.ts +25 -5
  192. package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
  193. package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
  194. package/src/tools/setup/capabilities/graphify.ts +12 -0
  195. package/src/tools/setup/capabilities/openspec.ts +273 -270
  196. package/src/tools/setup/capabilities/tokensave.ts +36 -3
  197. package/src/tools/setup/context-services.ts +9 -17
  198. package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
  199. package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
  200. package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
  201. package/src/tools/setup/dynamic-plugins/install.ts +24 -28
  202. package/src/tools/setup/engine.ts +93 -148
  203. package/src/tools/setup/managed-output.ts +51 -0
  204. package/src/tools/setup/mate.ts +86 -11
  205. package/src/tools/setup/plugin.ts +37 -17
  206. package/src/tools/setup/plugins/gitignore.ts +1 -7
  207. package/src/tools/setup/plugins/guidance.ts +7 -4
  208. package/src/tools/setup/providers/agent-file-sections.ts +4 -2
  209. package/src/tools/setup/providers/claude-attribution.ts +65 -0
  210. package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
  211. package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
  212. package/src/tools/setup/providers/claude.ts +285 -34
  213. package/src/tools/setup/providers/opencode.ts +180 -11
  214. package/src/tools/setup/registry.ts +2 -24
  215. package/src/tools/setup/surface-target.ts +4 -13
  216. package/src/tools/setup/utils.ts +14 -4
  217. package/src/tools/setup.ts +131 -18
  218. package/wrappers/bin/graphify +52 -4
  219. package/wrappers/bin/openspec +33 -9
  220. package/wrappers/lib/graphify-gate.mjs +8 -0
  221. package/opencode-plugin/src/audience-layer.ts +0 -91
  222. package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
  223. package/src/runtime/audience-layer.ts +0 -84
  224. package/src/runtime/opencode-audience-layer.ts +0 -176
  225. package/src/tools/setup/audience-isolation.fixture.ts +0 -325
  226. package/src/tools/setup/audience-layer.ts +0 -207
  227. package/src/tools/setup/audience-registrations.ts +0 -52
  228. package/src/tools/setup/contribution-ledger.ts +0 -207
  229. package/src/tools/setup/isolation-diagnostics.ts +0 -139
  230. package/src/tools/setup/runtime-documents.ts +0 -291
  231. package/src/tools/setup/upgrade-sweep.ts +0 -153
@@ -1,15 +1,21 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
 
5
+ import { parse as parseYaml } from "yaml";
6
+
4
7
  import { FRAMEWORK_NAME } from "../../framework";
5
8
  import { gitEnvironment } from "../../runtime/companion-git";
6
- import { readWrapStateSync } from "../../runtime/wrap-state";
9
+ import { companionClaudeMdHasOwnText } from "../../runtime/companion-instructions";
7
10
  import { getActiveDistribution } from "../../distribution";
11
+ import { hasOpenspecCapability } from "../../lib/orchestrator/capabilities";
8
12
  import { checkEngineRequirement } from "../../lib/orchestrator/engine-guard";
9
13
  import type { GlobalConfigStore } from "../../lib/orchestrator/global-config-store";
10
14
  import { pathIsDirectory } from "../../lib/fs-utils";
11
15
  import { resolveRootContext, type RootContext } from "../../lib/orchestrator/root-context";
12
16
  import type { CapabilityConfig, FrameworkConfig, HubMember } from "../../lib/orchestrator/types";
17
+ import { locateOpenSpec, type OpenSpecLocation } from "../../lib/openspec-cli";
18
+ import { MATE_CLAUDE_PLUGIN_NAME } from "../../lib/package-paths";
13
19
  import {
14
20
  describeUnknownAudience,
15
21
  readAudienceSelection,
@@ -20,12 +26,20 @@ import {
20
26
  type PluginInspection,
21
27
  type PluginVerificationFailure,
22
28
  } from "../../tools/setup/dynamic-plugins/verify";
29
+ import { hydrateDynamicPlugins } from "../../tools/setup/dynamic-plugins/hydrate";
23
30
  import {
24
- inspectAudienceIsolation,
25
- type AudienceIsolationReport,
26
- } from "../../tools/setup/isolation-diagnostics";
31
+ inspectClaudePluginDirectories,
32
+ type ClaudePluginDirectoryInspection,
33
+ } from "../../tools/setup";
27
34
  import { getRequiredPluginDrift } from "../../tools/setup/policy";
28
- import type { PluginRegistry } from "../../tools/setup/registry";
35
+ import {
36
+ CLAUDE_PLUGIN_MANIFEST,
37
+ CLAUDE_PLUGIN_MARKETPLACE,
38
+ type ClaudePluginContributionFailure,
39
+ type ClaudePluginDirectoryProblem,
40
+ type ClaudePluginDirectoryReport,
41
+ type ClaudePluginUnpublishedReason,
42
+ } from "../../tools/setup/providers/claude-plugin-folders";
29
43
  import { resolveCommandOnPath } from "../../tools/setup/utils";
30
44
  import { writeJsonStdout } from "../write-json-stdout";
31
45
  import { printSection, renderKeyValueTable, renderTable } from "./status";
@@ -37,10 +51,14 @@ interface DoctorDeps {
37
51
  /** Returns a member checkout's HEAD commit, or null when unreadable. */
38
52
  gitHead?: (memberPath: string) => string | null;
39
53
  inspectPlugins?: (companionPath: string) => Promise<PluginInspection>;
54
+ /** Loads the companion's declared plugins so their Capabilities contribute. */
55
+ hydratePlugins?: (companionPath: string) => Promise<void>;
56
+ inspectClaudePlugins?: (
57
+ companionPath: string,
58
+ config: FrameworkConfig,
59
+ ) => Promise<ClaudePluginDirectoryInspection>;
40
60
  /** Environment `MATE_AUDIENCE` is read from; defaults to the process environment. */
41
61
  env?: Record<string, string | undefined>;
42
- /** The hydrated registry; the active distribution's when absent. */
43
- registry?: PluginRegistry;
44
62
  }
45
63
 
46
64
  export type DoctorKind = "core" | "working" | "companion" | "hub";
@@ -50,6 +68,8 @@ export interface DoctorToolInstallation {
50
68
  source: string;
51
69
  status: "ok" | "missing";
52
70
  path: string | null;
71
+ /** Set for a tool Mate ships itself, such as the OpenSpec CLI. */
72
+ version?: string;
53
73
  }
54
74
 
55
75
  export interface DoctorHubMember {
@@ -74,18 +94,26 @@ export interface DoctorReport {
74
94
  toolInstallations?: DoctorToolInstallation[];
75
95
  requiredPluginDrift?: Array<{ pluginId: string; kind: string; reason: string }>;
76
96
  pluginFailures?: PluginVerificationFailure[];
77
- /**
78
- * Present for a companion that declares audiences or has one selected.
79
- * `wrapped` is the audience the working repository is wrapped for, when any.
80
- */
81
- audiences?: { declared: string[]; active: string | null; wrapped?: string };
97
+ /** Present for a companion that declares audiences or has one selected. */
98
+ audiences?: { declared: string[]; active: string | null };
82
99
  /** Set when `MATE_AUDIENCE` names an audience the companion does not declare. */
83
100
  audienceError?: string;
84
- /** Present when an audience's contributions reached a persisted surface, or a launch layer cannot deliver part of one. */
85
- audienceIsolation?: AudienceIsolationReport;
86
101
  /** IDs of the capabilities the verified plugins provide. */
87
102
  pluginCapabilities?: string[];
88
103
  engineRequirement?: { range: string; ok: boolean; detail: string };
104
+ /** Present when the companion's `openspec/config.yaml` has `rules` keys OpenSpec ignores. */
105
+ openspecRuleWarnings?: Array<{ key: string; message: string }>;
106
+ /** Present when the companion `CLAUDE.md` holds text no Working Repository session receives. */
107
+ companionInstructionWarnings?: Array<{ file: string; message: string }>;
108
+ /**
109
+ * Enabled Capabilities' Claude plugin folders, which load only with `mate claude`; absent when
110
+ * none. `problem` names why `mate claude` leaves a folder out: `not-absolute`,
111
+ * `missing-manifest`, `bundled-mate-plugin` (Mate's own plugin, which every managed launch
112
+ * loads already) or `mate-plugin-name` (its manifest uses Mate's plugin name).
113
+ */
114
+ claudePlugins?: ClaudePluginDirectoryReport[];
115
+ /** Capabilities whose contributions failed, so their Claude plugin folders are unknown; absent when none. */
116
+ claudePluginFailures?: ClaudePluginContributionFailure[];
89
117
  hub?: { members: DoctorHubMember[] };
90
118
  resolutionFailures: Array<{ companionPath: string; message: string }>;
91
119
  }
@@ -94,6 +122,8 @@ interface ToolCheck {
94
122
  name: string;
95
123
  command: string;
96
124
  source: string;
125
+ /** Locates a tool Mate ships instead of looking it up on PATH. */
126
+ bundled?: () => OpenSpecLocation;
97
127
  }
98
128
 
99
129
  /** Doctor's view of the shared root kind: registry/env resolution means cwd is a working repo. */
@@ -134,7 +164,12 @@ function getSelectedToolChecks(config: {
134
164
  checks.push({ name: "uv", command: "uv", source: "package manager" });
135
165
  }
136
166
  if (selectedCapabilities.has("openspec")) {
137
- checks.push({ name: "openspec", command: "openspec", source: "capability" });
167
+ checks.push({
168
+ name: "openspec",
169
+ command: "openspec",
170
+ source: "capability",
171
+ bundled: locateOpenSpec,
172
+ });
138
173
  }
139
174
  if (selectedCapabilities.has("tokensave")) {
140
175
  checks.push({ name: "tokensave", command: "tokensave", source: "capability" });
@@ -154,6 +189,21 @@ function collectToolInstallations(
154
189
  pathValue: string,
155
190
  ): DoctorToolInstallation[] {
156
191
  return getSelectedToolChecks(config).map((check) => {
192
+ if (check.bundled) {
193
+ const source = `${check.source}, bundled with ${FRAMEWORK_NAME}`;
194
+ const location = check.bundled();
195
+ if ("missing" in location) {
196
+ return { tool: check.name, source, status: "missing", path: null };
197
+ }
198
+ // An unreadable manifest only loses the version; the CLI itself resolved.
199
+ return {
200
+ tool: check.name,
201
+ source,
202
+ status: "ok",
203
+ path: location.bin,
204
+ ...(location.version ? { version: location.version } : {}),
205
+ };
206
+ }
157
207
  const resolved = resolveCommandOnPath(check.command, pathValue);
158
208
  return {
159
209
  tool: check.name,
@@ -243,21 +293,92 @@ function collectAudiences(
243
293
  report: DoctorReport,
244
294
  config: FrameworkConfig,
245
295
  env: Record<string, string | undefined>,
246
- repositoryPath: string | null,
247
296
  ): void {
248
297
  const declared = Object.keys(config.audiences ?? {});
249
298
  const requested = readAudienceSelection(env);
250
299
  if (declared.length === 0 && requested === null) return;
251
300
  const resolution = resolveAudience(config, requested);
252
- const wrapped = repositoryPath ? readWrapStateSync(repositoryPath).audience : null;
253
- report.audiences = {
254
- declared,
255
- active: resolution.ok ? resolution.active : null,
256
- ...(wrapped ? { wrapped } : {}),
257
- };
301
+ report.audiences = { declared, active: resolution.ok ? resolution.active : null };
258
302
  if (!resolution.ok) report.audienceError = describeUnknownAudience(resolution);
259
303
  }
260
304
 
305
+ /** Artifact IDs of OpenSpec's built-in `spec-driven` schema. */
306
+ const BUILT_IN_OPENSPEC_ARTIFACTS = ["proposal", "specs", "design", "tasks"];
307
+
308
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
309
+ return value && typeof value === "object" && !Array.isArray(value)
310
+ ? (value as Record<string, unknown>)
311
+ : undefined;
312
+ }
313
+
314
+ /** Built-in IDs plus every `artifacts[].id` from a `schema.yaml` under the companion's `openspec/schemas/`. */
315
+ async function knownOpenSpecArtifactIds(companionPath: string): Promise<Set<string>> {
316
+ const schemasDir = path.join(companionPath, "openspec", "schemas");
317
+ let schemaNames: string[] = [];
318
+ try {
319
+ schemaNames = await fs.readdir(schemasDir);
320
+ } catch {
321
+ /* no companion schemas */
322
+ }
323
+ const schemaIds = await Promise.all(
324
+ schemaNames.map(async (name) => {
325
+ try {
326
+ const schema = asRecord(
327
+ parseYaml(await fs.readFile(path.join(schemasDir, name, "schema.yaml"), "utf8")),
328
+ );
329
+ const artifacts: unknown[] = Array.isArray(schema?.artifacts) ? schema.artifacts : [];
330
+ return artifacts.flatMap((artifact) => {
331
+ const id = asRecord(artifact)?.id;
332
+ return typeof id === "string" ? [id] : [];
333
+ });
334
+ } catch {
335
+ return []; // not a readable schema
336
+ }
337
+ }),
338
+ );
339
+ return new Set([...BUILT_IN_OPENSPEC_ARTIFACTS, ...schemaIds.flat()]);
340
+ }
341
+
342
+ /**
343
+ * OpenSpec ignores a `rules` key that names no artifact, warning only on stderr
344
+ * of the command that reads it. Read-only; a missing or unparseable config is silent.
345
+ */
346
+ async function collectOpenSpecRuleWarnings(
347
+ companionPath: string,
348
+ ): Promise<NonNullable<DoctorReport["openspecRuleWarnings"]>> {
349
+ let rules: Record<string, unknown> | undefined;
350
+ try {
351
+ const config = parseYaml(
352
+ await fs.readFile(path.join(companionPath, "openspec", "config.yaml"), "utf8"),
353
+ );
354
+ rules = asRecord(asRecord(config)?.rules);
355
+ } catch {
356
+ return [];
357
+ }
358
+ if (!rules) return [];
359
+
360
+ const known = await knownOpenSpecArtifactIds(companionPath);
361
+ return Object.keys(rules)
362
+ .filter((key) => !known.has(key))
363
+ .map((key) => ({
364
+ key,
365
+ message: `rules.${key} matches no OpenSpec artifact and is ignored; put workflow guidance in operations.apply.guidance or operations.archive.guidance`,
366
+ }));
367
+ }
368
+
369
+ function collectCompanionInstructionWarnings(
370
+ companionPath: string,
371
+ ): NonNullable<DoctorReport["companionInstructionWarnings"]> {
372
+ if (!companionClaudeMdHasOwnText(companionPath)) return [];
373
+ const file = path.join(companionPath, "CLAUDE.md");
374
+ return [
375
+ {
376
+ file,
377
+ message: `${file} holds text besides @AGENTS.md and Mate-managed blocks. That text does not reach Working Repository sessions, only sessions started in the companion. Move rules meant for every session into ${path.join(companionPath, "AGENTS.md")}.`,
378
+ },
379
+ ];
380
+ }
381
+
261
382
  export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<DoctorReport> {
262
383
  const cwd = path.resolve(deps.cwd ?? process.cwd());
263
384
  const pathValue = deps.pathValue ?? process.env.PATH ?? "";
@@ -293,16 +414,33 @@ export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<Doctor
293
414
 
294
415
  const env = deps.env ?? process.env;
295
416
  if (root.rootPath && root.config && kind !== "hub") {
296
- collectAudiences(report, root.config, env, root.linkedRepository?.path ?? null);
297
- const isolation = await inspectAudienceIsolation(
298
- root.rootPath,
299
- root.config,
300
- deps.registry ?? getActiveDistribution().registry,
301
- report.audiences?.active ?? null,
302
- );
303
- if (isolation.leaked.length + isolation.unswept.length + isolation.undelivered.length > 0) {
304
- report.audienceIsolation = isolation;
305
- }
417
+ collectAudiences(report, root.config, env);
418
+ }
419
+
420
+ if (
421
+ root.rootPath &&
422
+ (kind === "working" || kind === "companion") &&
423
+ hasOpenspecCapability(root.config?.capabilities)
424
+ ) {
425
+ const warnings = await collectOpenSpecRuleWarnings(root.rootPath);
426
+ if (warnings.length > 0) report.openspecRuleWarnings = warnings;
427
+ }
428
+
429
+ if (root.rootPath && (kind === "working" || kind === "companion")) {
430
+ const warnings = collectCompanionInstructionWarnings(root.rootPath);
431
+ if (warnings.length > 0) report.companionInstructionWarnings = warnings;
432
+ }
433
+
434
+ if (root.rootPath && root.config && (kind === "working" || kind === "companion")) {
435
+ /** Doctor reports plugin failures below; hydration supplies their contributions. */
436
+ const hydrate =
437
+ deps.hydratePlugins ??
438
+ ((companionPath) => hydrateDynamicPlugins({ companionPath, env, warn: () => {} }));
439
+ await hydrate(root.rootPath);
440
+ const inspectClaudePlugins = deps.inspectClaudePlugins ?? inspectClaudePluginDirectories;
441
+ const inspection = await inspectClaudePlugins(root.rootPath, root.config);
442
+ if (inspection.plugins.length > 0) report.claudePlugins = inspection.plugins;
443
+ if (inspection.failures.length > 0) report.claudePluginFailures = inspection.failures;
306
444
  }
307
445
 
308
446
  if (root.rootPath && root.config) {
@@ -335,7 +473,12 @@ function renderToolInstallations(installations: DoctorToolInstallation[]): strin
335
473
  }
336
474
  return renderTable(
337
475
  ["Tool", "Source", "Status", "Path"],
338
- installations.map((item) => [item.tool, item.source, item.status, item.path ?? ""]),
476
+ installations.map((item) => [
477
+ item.version ? `${item.tool} ${item.version}` : item.tool,
478
+ item.source,
479
+ item.status,
480
+ item.path ?? "",
481
+ ]),
339
482
  );
340
483
  }
341
484
 
@@ -353,47 +496,63 @@ function renderHubMembers(members: DoctorHubMember[]): string {
353
496
  );
354
497
  }
355
498
 
356
- function renderAudienceIsolation(isolation: AudienceIsolationReport): string {
357
- const parts: string[] = [];
358
- if (isolation.leaked.length > 0) {
359
- parts.push(
360
- "Contributions on persisted surfaces that no base plugin owns; the next sync removes them:",
499
+ /** The Status column's text for each reason a folder is left out. */
500
+ const CLAUDE_PLUGIN_PROBLEM_STATUS: Record<ClaudePluginDirectoryProblem, string> = {
501
+ "not-absolute": "not an absolute path",
502
+ "bundled-mate-plugin": "Mate's bundled plugin, already loaded",
503
+ "missing-manifest": `missing ${CLAUDE_PLUGIN_MANIFEST}`,
504
+ "mate-plugin-name": `uses Mate's plugin name ${MATE_CLAUDE_PLUGIN_NAME}`,
505
+ };
506
+
507
+ function claudePluginStatus(plugin: ClaudePluginDirectoryReport): string {
508
+ return plugin.problem ? CLAUDE_PLUGIN_PROBLEM_STATUS[plugin.problem] : "present";
509
+ }
510
+
511
+ /** The Plain Sessions column's text for each reason a loadable folder is not published. */
512
+ const CLAUDE_PLUGIN_UNPUBLISHED_STATUS: Record<ClaudePluginUnpublishedReason, string> = {
513
+ "foreign-marketplace": `no: ${CLAUDE_PLUGIN_MARKETPLACE} is not Mate's and does not list it`,
514
+ "invalid-marketplace-name": "no: its name cannot form a marketplace name",
515
+ "name-conflict": "no: its marketplace name is already in use",
516
+ "missing-marketplace": `no: ${CLAUDE_PLUGIN_MARKETPLACE} could not be written`,
517
+ };
518
+
519
+ function claudePluginPlainSessions(plugin: ClaudePluginDirectoryReport): string {
520
+ if (plugin.marketplace) return `marketplace ${plugin.marketplace}`;
521
+ if (plugin.unpublishedReason) return CLAUDE_PLUGIN_UNPUBLISHED_STATUS[plugin.unpublishedReason];
522
+ return "no";
523
+ }
524
+
525
+ /** A table for each list present, then a note on where plain sessions load them. */
526
+ function renderClaudePlugins(
527
+ plugins: ClaudePluginDirectoryReport[] | undefined,
528
+ failures: ClaudePluginContributionFailure[] | undefined,
529
+ ): string {
530
+ const tables: string[] = [];
531
+ if (plugins) {
532
+ tables.push(
361
533
  renderTable(
362
- ["Surface", "Kind", "Entry", "Owner"],
363
- isolation.leaked.map((entry) => [
364
- entry.surface,
365
- entry.kind,
366
- entry.identity ?? "",
367
- entry.owner,
534
+ ["Capability", "Plugin Folder", "Status", "Plain Sessions"],
535
+ plugins.map((plugin) => [
536
+ plugin.capability,
537
+ plugin.path,
538
+ claudePluginStatus(plugin),
539
+ claudePluginPlainSessions(plugin),
368
540
  ]),
369
541
  ),
370
542
  );
371
543
  }
372
- if (isolation.unswept.length > 0) {
373
- parts.push(
374
- `Audience plugins not swept (could not be loaded); run \`MATE_AUDIENCE=<audience> ${FRAMEWORK_NAME} install --yes\` with their credentials:`,
375
- renderList(isolation.unswept, ""),
376
- );
377
- }
378
- if (isolation.undelivered.length > 0) {
379
- parts.push(
380
- "Active audience plugins contribute declaratively only; these parts are not delivered:",
381
- renderList(
382
- isolation.undelivered.map(
383
- (entry) =>
384
- `${entry.pluginId}: ${entry.parts
385
- .map((part) =>
386
- part === "pluginReferences"
387
- ? "OpenCode plugin references are not delivered"
388
- : `${part} is not run`,
389
- )
390
- .join("; ")}`,
391
- ),
392
- "",
544
+ if (failures) {
545
+ tables.push(
546
+ renderTable(
547
+ ["Capability", "Error"],
548
+ failures.map((failure) => [failure.capability, failure.reason]),
393
549
  ),
394
550
  );
395
551
  }
396
- return parts.join("\n\n");
552
+ return [
553
+ ...tables.flatMap((table) => [table, ""]),
554
+ `\`${FRAMEWORK_NAME} claude\` loads every present plugin. Plain \`claude\` sessions load one with a marketplace, and only in a wrapped repository that Claude Code trusts.`,
555
+ ].join("\n");
397
556
  }
398
557
 
399
558
  function renderHumanReport(report: DoctorReport): void {
@@ -442,20 +601,9 @@ function renderHumanReport(report: DoctorReport): void {
442
601
  renderKeyValueTable([
443
602
  ["Declared", report.audiences.declared.join(", ") || "none"],
444
603
  ["Active", report.audiences.active ?? "none"],
445
- ...(report.audiences.wrapped
446
- ? [
447
- ["Wrapped", `${report.audiences.wrapped} (bare sessions of this checkout)`] as [
448
- string,
449
- string,
450
- ],
451
- ]
452
- : []),
453
604
  ]),
454
605
  );
455
606
  }
456
- if (report.audienceIsolation) {
457
- printSection("Audience Isolation", renderAudienceIsolation(report.audienceIsolation));
458
- }
459
607
  if (report.hub) printSection("Hub Members", renderHubMembers(report.hub.members));
460
608
  // Companions with no drift produce no section at all: required plugins are
461
609
  // silent when healthy.
@@ -480,6 +628,30 @@ function renderHumanReport(report: DoctorReport): void {
480
628
  if (report.engineRequirement) {
481
629
  printSection("Version Requirement", report.engineRequirement.detail);
482
630
  }
631
+ if (report.claudePlugins || report.claudePluginFailures) {
632
+ printSection(
633
+ "Claude Plugins",
634
+ renderClaudePlugins(report.claudePlugins, report.claudePluginFailures),
635
+ );
636
+ }
637
+ if (report.openspecRuleWarnings) {
638
+ printSection(
639
+ "OpenSpec Config",
640
+ renderList(
641
+ report.openspecRuleWarnings.map((warning) => warning.message),
642
+ "",
643
+ ),
644
+ );
645
+ }
646
+ if (report.companionInstructionWarnings) {
647
+ printSection(
648
+ "Companion Instructions",
649
+ renderList(
650
+ report.companionInstructionWarnings.map((warning) => warning.message),
651
+ "",
652
+ ),
653
+ );
654
+ }
483
655
 
484
656
  printSection(
485
657
  "Resolution Failures",
@@ -500,7 +672,8 @@ function renderHumanReport(report: DoctorReport): void {
500
672
  * capabilities and tool checks for companions, member health for hubs.
501
673
  * Declared plugins of the effective set (base plus the `MATE_AUDIENCE`
502
674
  * audience) are verified strictly (installed, loadable); any failure is named
503
- * on stderr and exits non-zero, as is an undeclared `MATE_AUDIENCE`.
675
+ * on stderr and exits non-zero, as is an undeclared `MATE_AUDIENCE` or a
676
+ * Capability whose runtime contributions fail to load.
504
677
  * `--json` emits the same report as one JSON document.
505
678
  */
506
679
  export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {}): Promise<void> {
@@ -512,6 +685,12 @@ export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {
512
685
  for (const failure of failures) {
513
686
  process.stderr.write(`${FRAMEWORK_NAME}: plugin ${failure.package}: ${failure.reason}\n`);
514
687
  }
688
+ for (const failure of report.claudePluginFailures ?? []) {
689
+ process.stderr.write(
690
+ `${FRAMEWORK_NAME}: capability ${failure.capability}: ${failure.reason}\n`,
691
+ );
692
+ process.exitCode = 1;
693
+ }
515
694
  if (report.audienceError) {
516
695
  process.stderr.write(`${FRAMEWORK_NAME}: ${report.audienceError}\n`);
517
696
  process.exitCode = 1;
@@ -1,8 +1,5 @@
1
- import path from "node:path";
2
-
3
1
  import { confirm } from "../confirm";
4
2
  import { FRAMEWORK_NAME } from "../../framework";
5
- import { ConfigStore } from "../../lib/orchestrator/config-store";
6
3
  import {
7
4
  buildInstallPlan,
8
5
  inspectInstallPlan,
@@ -179,11 +176,6 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
179
176
  }
180
177
 
181
178
  try {
182
- if (!frozen && context.kind === "companion" && context.companionPath) {
183
- await new ConfigStore(
184
- path.join(context.companionPath, `.${FRAMEWORK_NAME}`, "config", "framework.yaml"),
185
- ).save(context.config);
186
- }
187
179
  await reconcileInstalledCompanion(plan);
188
180
  await saveCompleteInstallState(plan, execution.results);
189
181
  } catch (error) {
@@ -32,6 +32,8 @@ function withDeclaration(
32
32
  );
33
33
  }
34
34
 
35
+ const INSTALL_USAGE = `usage: ${FRAMEWORK_NAME} plugin install <package>[@version]`;
36
+
35
37
  export interface PluginInstallCommandDeps {
36
38
  cwd?: string;
37
39
  installDeps?: PluginInstallDeps;
@@ -50,10 +52,13 @@ export async function runPluginInstallCommand(
50
52
  deps: PluginInstallCommandDeps = {},
51
53
  ): Promise<boolean> {
52
54
  const spec = argv[0];
53
- if (!spec) {
54
- process.stderr.write(
55
- `${FRAMEWORK_NAME}: usage: ${FRAMEWORK_NAME} plugin install <package>[@version]\n`,
56
- );
55
+ if (spec === "-h" || spec === "--help") {
56
+ process.stdout.write(`${INSTALL_USAGE}\n`);
57
+ return true;
58
+ }
59
+ if (!spec || spec.startsWith("-")) {
60
+ const problem = spec ? `unknown option ${spec}; ` : "";
61
+ process.stderr.write(`${FRAMEWORK_NAME}: ${problem}${INSTALL_USAGE}\n`);
57
62
  process.exitCode = 1;
58
63
  return false;
59
64
  }
@@ -1,13 +1,14 @@
1
1
  import { usage } from "../../usage";
2
- import { runPluginInstallCommand } from "./install";
2
+ import { type PluginInstallCommandDeps, runPluginInstallCommand } from "./install";
3
3
 
4
4
  export async function runPluginCommand(
5
5
  subcommand: string | undefined,
6
6
  argv: string[],
7
+ deps: PluginInstallCommandDeps = {},
7
8
  ): Promise<void> {
8
9
  switch (subcommand) {
9
10
  case "install":
10
- await runPluginInstallCommand(argv);
11
+ if (!(await runPluginInstallCommand(argv, deps))) process.exitCode = 1;
11
12
  return;
12
13
  default:
13
14
  console.error(`Unknown plugin command: ${subcommand ?? ""}`);
@@ -5,13 +5,21 @@ import type {
5
5
  ReportKeyValue,
6
6
  ReportMetric,
7
7
  ReportSection,
8
+ ReportStatusTone,
8
9
  } from "./types";
9
10
  import { REPORT_DOCUMENT_VERSION } from "./types";
10
11
 
12
+ /** Collector status strings mapped to tones, so the built-in report never relies on inference. */
13
+ const TOOL_STATUS_TONES: Record<string, ReportStatusTone> = {
14
+ ok: "ok",
15
+ "no data": "warn",
16
+ "no window data": "warn",
17
+ "not installed": "neutral",
18
+ "not enabled": "neutral",
19
+ };
20
+
11
21
  export function reportDataToDocument(data: ReportData): ReportDocument {
12
22
  const metadata: ReportKeyValue[] = [
13
- { label: "Window", value: `Last ${data.days} days` },
14
- { label: "Generated", value: data.generatedAt },
15
23
  { label: "Working repository", value: data.workingRepoPath },
16
24
  { label: "Companion repository", value: data.companionRepoPath },
17
25
  { label: "Active agents", value: data.activeAgents.join(", ") || "None" },
@@ -57,6 +65,7 @@ export function reportDataToDocument(data: ReportData): ReportDocument {
57
65
  items: data.toolStatus.map((entry) => ({
58
66
  label: entry.name,
59
67
  status: entry.status,
68
+ tone: TOOL_STATUS_TONES[entry.status] ?? "neutral",
60
69
  })),
61
70
  },
62
71
  ];
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  REPORT_DOCUMENT_VERSION,
3
+ REPORT_STATUS_TONES,
4
+ REPORT_TEXT_FORMATS,
3
5
  type ReportDiagramSection,
4
6
  type ReportDiffSection,
5
7
  type ReportDocument,
@@ -46,6 +48,21 @@ function requiredString(
46
48
  return true;
47
49
  }
48
50
 
51
+ function optionalEnum(
52
+ value: unknown,
53
+ allowed: readonly string[],
54
+ path: string,
55
+ issues: ReportValidationIssue[],
56
+ ): void {
57
+ if (value === undefined || allowed.includes(value as string)) return;
58
+ const list = `${allowed.slice(0, -1).join(", ")}${allowed.length > 2 ? "," : ""} or ${allowed.at(-1)}`;
59
+ issues.push({ path, message: `must be one of ${list}` });
60
+ }
61
+
62
+ function optionalCaption(value: unknown, path: string, issues: ReportValidationIssue[]): void {
63
+ if (value !== undefined) requiredString(value, path, issues);
64
+ }
65
+
49
66
  function validateKeyValues(value: unknown, path: string, issues: ReportValidationIssue[]): boolean {
50
67
  if (!Array.isArray(value)) {
51
68
  issues.push({ path, message: "must be an array" });
@@ -112,6 +129,7 @@ function validateStatuses(value: unknown, path: string, issues: ReportValidation
112
129
  if (item.detail !== undefined && typeof item.detail !== "string") {
113
130
  issues.push({ path: `${itemPath}.detail`, message: "must be a string" });
114
131
  }
132
+ optionalEnum(item.tone, REPORT_STATUS_TONES, `${itemPath}.tone`, issues);
115
133
  });
116
134
  return true;
117
135
  }
@@ -209,14 +227,17 @@ function validateSection(value: unknown, path: string, issues: ReportValidationI
209
227
  if (typeof value.content !== "string") {
210
228
  issues.push({ path: `${path}.content`, message: "must be a string" });
211
229
  }
230
+ optionalEnum(value.format, REPORT_TEXT_FORMATS, `${path}.format`, issues);
212
231
  return;
213
232
  case "diagram":
214
233
  validateDiagram(value, path, issues);
234
+ optionalCaption(value.caption, `${path}.caption`, issues);
215
235
  return;
216
236
  case "diff":
217
237
  if (typeof value.patch !== "string") {
218
238
  issues.push({ path: `${path}.patch`, message: "must be a string" });
219
239
  }
240
+ optionalCaption(value.caption, `${path}.caption`, issues);
220
241
  return;
221
242
  default:
222
243
  issues.push({
@@ -1,5 +1,12 @@
1
- import { normalizeLanguage, renderNodesToHtml, renderTokens, tokenize } from "@tanstack/highlight";
2
- import { createThemeCss } from "@tanstack/highlight/theme";
1
+ import {
2
+ type HighlightRenderNode,
3
+ normalizeLanguage,
4
+ renderNodesToHtml,
5
+ renderTokens,
6
+ tokenize,
7
+ } from "@tanstack/highlight";
8
+ import { createThemeCss, createThemeRule } from "@tanstack/highlight/theme";
9
+ import githubDark from "@tanstack/highlight/themes/github-dark";
3
10
  import githubLight from "@tanstack/highlight/themes/github-light";
4
11
 
5
12
  /** Sole boundary against `@tanstack/highlight`, so an upstream API break stays a single-file fix. */
@@ -18,7 +25,30 @@ export function highlightInline(code: string, language: string | undefined): str
18
25
  return renderNodesToHtml(renderTokens(tokens));
19
26
  }
20
27
 
21
- /** Token colours as an inlinable stylesheet; carries no script, font, or external reference. */
28
+ /**
29
+ * The same tokens as structure rather than markup, for a renderer that builds elements
30
+ * and so never holds a markup string. An unknown language yields plain text nodes.
31
+ */
32
+ export function highlightNodes(
33
+ code: string,
34
+ language: string | undefined,
35
+ ): ReadonlyArray<HighlightRenderNode> {
36
+ const { tokens } = tokenize(code, { lang: resolveHighlightLanguage(language) });
37
+ return renderTokens(tokens);
38
+ }
39
+
40
+ /**
41
+ * Token colours as an inlinable stylesheet; carries no script, font, or external reference.
42
+ * Light by default, dark on screen when the reader prefers it or the page sets
43
+ * `data-scheme="dark"`, and light in print because both dark rules are screen-only.
44
+ */
22
45
  export function highlightStyleBlock(codeBlockSelector: string): string {
23
- return createThemeCss({ light: githubLight, codeBlockSelector });
46
+ const light = createThemeCss({ light: githubLight, codeBlockSelector });
47
+ return `${light}
48
+ @media screen and (prefers-color-scheme: dark) {
49
+ ${createThemeRule(':root:not([data-scheme="light"])', githubDark)}
50
+ }
51
+ @media screen {
52
+ ${createThemeRule(':root[data-scheme="dark"]', githubDark)}
53
+ }`;
24
54
  }