@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.
- package/claude-plugin/hooks/companion-instructions.mjs +9 -0
- package/claude-plugin/hooks/hooks.json +12 -2
- package/opencode-plugin/src/add-dir.ts +76 -10
- package/opencode-plugin/src/companion.ts +1 -8
- package/opencode-plugin/src/server.ts +2 -15
- package/package.json +10 -2
- package/src/brand/art.generated.ts +69 -0
- package/src/brand/index.ts +12 -0
- package/src/brand/palette.ts +19 -0
- package/src/brand/svg.generated.ts +8 -0
- package/src/brand/terminal.ts +83 -0
- package/src/brand/types.ts +33 -0
- package/src/cli/commands/artifact/artifact.ts +8 -0
- package/src/cli/commands/artifact/delivery/archive.ts +117 -0
- package/src/cli/commands/artifact/delivery/branch-flags.ts +55 -0
- package/src/cli/commands/artifact/delivery/environment.ts +104 -0
- package/src/cli/commands/artifact/delivery/link.ts +154 -0
- package/src/cli/commands/artifact/delivery/output.ts +58 -0
- package/src/cli/commands/artifact/finish/command.ts +33 -3
- package/src/cli/commands/artifact/finish/engine.ts +47 -3
- package/src/cli/commands/artifact/finish/finisher.ts +10 -0
- package/src/cli/commands/artifact/finish/openspec.ts +200 -129
- package/src/cli/commands/artifact/finish/secret-scan.ts +43 -0
- package/src/cli/commands/artifact/pending/command.ts +31 -3
- package/src/cli/commands/artifact/pending/discovery.ts +21 -57
- package/src/cli/commands/cap/graphify.ts +52 -3
- package/src/cli/commands/cap/index-cmd.ts +8 -1
- package/src/cli/commands/cap/openspec.ts +13 -25
- package/src/cli/commands/companion/open.ts +56 -39
- package/src/cli/commands/companion/update.ts +75 -42
- package/src/cli/commands/doctor.ts +257 -78
- package/src/cli/commands/install.ts +0 -8
- package/src/cli/commands/plugin/install.ts +9 -4
- package/src/cli/commands/plugin/plugin.ts +3 -2
- package/src/cli/commands/report/adapter.ts +11 -2
- package/src/cli/commands/report/contract.ts +21 -0
- package/src/cli/commands/report/highlight.ts +34 -4
- package/src/cli/commands/report/markdown.ts +110 -0
- package/src/cli/commands/report/palette.ts +118 -0
- package/src/cli/commands/report/renderer.ts +456 -153
- package/src/cli/commands/report/types.ts +16 -0
- package/src/cli/commands/setup.ts +7 -15
- package/src/cli/commands/studio/client-asset.ts +16 -0
- package/src/cli/commands/studio/openspec-cli.ts +10 -58
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +19 -1
- package/src/cli/commands/studio/server.ts +42 -1
- package/src/cli/commands/studio/terminal-launch.ts +7 -16
- package/src/cli/commands/studio/views/client.ts +6 -616
- package/src/cli/commands/studio/views/dashboard/changes.tsx +5 -10
- package/src/cli/commands/studio/views/document.tsx +137 -69
- package/src/cli/commands/studio/views/markdown.tsx +250 -0
- package/src/cli/commands/studio/views/palette.ts +134 -0
- package/src/cli/commands/studio/views/reports/index.tsx +2 -7
- package/src/cli/commands/studio/views/skills/index.tsx +43 -62
- package/src/cli/commands/studio/views/specs/index.tsx +7 -15
- package/src/cli/commands/studio/views/studio-client/actions.ts +51 -0
- package/src/cli/commands/studio/views/studio-client/companion.ts +13 -0
- package/src/cli/commands/studio/views/studio-client/index.ts +32 -0
- package/src/cli/commands/studio/views/studio-client/keys.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/main.ts +3 -0
- package/src/cli/commands/studio/views/studio-client/page.ts +146 -0
- package/src/cli/commands/studio/views/studio-client/panels.ts +57 -0
- package/src/cli/commands/studio/views/studio-client/reports.ts +122 -0
- package/src/cli/commands/studio/views/studio-client/theme.ts +63 -0
- package/src/cli/commands/studio/views/studio-client/vault-editor.ts +166 -0
- package/src/cli/commands/studio/views/studio-client/vault-pane.ts +202 -0
- package/src/cli/commands/studio/views/studio-client/vault-tree.ts +258 -0
- package/src/cli/commands/studio/views/studio-client.generated.ts +2 -0
- package/src/cli/commands/studio/views/styles.ts +217 -206
- package/src/cli/commands/studio/views/vault/state.ts +3 -5
- package/src/cli/commands/studio/views/workflow/delivery-path.tsx +370 -0
- package/src/cli/commands/studio/views/workflow/graph.ts +220 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +2 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +228 -287
- package/src/cli/commands/wrap.ts +9 -42
- package/src/cli/main.ts +4 -1
- package/src/cli/parse-flags.ts +25 -7
- package/src/cli/setup-selector.tsx +38 -162
- package/src/cli/usage.ts +5 -2
- package/src/hooks/companion-instructions.ts +190 -0
- package/src/hooks/hook-io.ts +63 -0
- package/src/hooks/session-banner.ts +21 -29
- package/src/hooks/session-guidance.ts +10 -36
- package/src/hooks/session-prepare.ts +115 -28
- package/src/hooks/validate-artifact-path.ts +82 -24
- package/src/index.ts +4 -0
- package/src/lib/change-layout.ts +154 -0
- package/src/lib/components/wizard-header.tsx +3 -1
- package/src/lib/context-mode-package.ts +24 -11
- package/src/lib/delivery/archive-change.ts +154 -0
- package/src/lib/delivery/branches.ts +108 -0
- package/src/lib/delivery/environment.ts +29 -0
- package/src/lib/delivery/forge.ts +279 -0
- package/src/lib/delivery/overlap.ts +202 -0
- package/src/lib/delivery/record-delivery.ts +196 -0
- package/src/lib/delivery/record.ts +343 -0
- package/src/lib/delivery/refresh.ts +174 -0
- package/src/lib/delivery/repository-access.ts +97 -0
- package/src/lib/delivery/stages.ts +111 -0
- package/src/lib/delivery/token.ts +58 -0
- package/src/lib/frontmatter.ts +18 -0
- package/src/lib/opencode-plugin-package.ts +5 -5
- package/src/lib/openspec-cli.ts +116 -0
- package/src/lib/openspec-scopes.ts +86 -0
- package/src/lib/orchestrator/adapters/claude-args.ts +50 -0
- package/src/lib/orchestrator/adapters/claude.ts +60 -162
- package/src/lib/orchestrator/adapters/opencode.ts +9 -24
- package/src/lib/orchestrator/config-store.ts +2 -70
- package/src/lib/orchestrator/editor.ts +9 -9
- package/src/lib/orchestrator/openspec-store.ts +14 -54
- package/src/lib/orchestrator/projection-claude-commands.ts +36 -0
- package/src/lib/orchestrator/projection-claude-links.ts +159 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +19 -190
- package/src/lib/orchestrator/projection-entries.ts +45 -15
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -2
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +49 -87
- package/src/lib/orchestrator/setup-compatibilities.ts +18 -75
- package/src/lib/orchestrator/types.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +22 -88
- package/src/lib/package-paths.ts +8 -0
- package/src/opencode/companion-hooks.ts +64 -5
- package/src/opencode/companion-policy.ts +37 -18
- package/src/opencode/projected-guidance.ts +4 -1
- package/src/opencode/session-prepare.ts +2 -1
- package/src/opencode/tui.tsx +4 -2
- package/src/runtime/artifact-refusal.ts +63 -0
- package/src/runtime/claude-delivery-plan.ts +89 -7
- package/src/runtime/commit-attribution-guard.ts +181 -0
- package/src/runtime/companion-guidance.ts +15 -3
- package/src/runtime/companion-instructions.ts +340 -0
- package/src/runtime/error-message.ts +4 -0
- package/src/runtime/guidance.ts +0 -17
- package/src/runtime/index.ts +5 -0
- package/src/runtime/managed-block.ts +117 -0
- package/src/runtime/markdown-scan.ts +266 -0
- package/src/runtime/policy.ts +16 -15
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -28
- package/src/runtime/secret-guard.ts +148 -0
- package/src/runtime/secret-patterns.ts +164 -0
- package/src/runtime/secret-scan.ts +90 -0
- package/src/runtime/session-kind.ts +13 -0
- package/src/runtime/shell-heredoc.ts +81 -0
- package/src/runtime/wrap-state.ts +36 -6
- package/src/runtime/write-atomic.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +8 -6
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +7 -9
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +10 -12
- package/src/templates/mate-skills/THIRD-PARTY-NOTICES.md +40 -0
- package/src/templates/mate-skills/agents/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/agents/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/agents/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{claude/mate-openspec-backfill → agents/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/agents/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/agents/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/agents/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/agents/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/agents/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/agents/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/claude/mate-apply-change/SKILL.md +76 -0
- package/src/templates/mate-skills/claude/mate-apply-change/references/openspec-apply-change.md +198 -0
- package/src/templates/mate-skills/claude/mate-archive-change/SKILL.md +59 -0
- package/src/templates/mate-skills/{agents/mate-openspec-backfill → claude/mate-backfill-spec}/SKILL.md +4 -4
- package/src/templates/mate-skills/claude/mate-clarify-intent/SKILL.md +95 -0
- package/src/templates/mate-skills/claude/mate-explore-change/SKILL.md +50 -0
- package/src/templates/mate-skills/claude/mate-explore-change/references/openspec-explore.md +348 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +3 -3
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +5 -5
- package/src/templates/mate-skills/claude/mate-propose-change/SKILL.md +103 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-propose.md +167 -0
- package/src/templates/mate-skills/claude/mate-propose-change/references/openspec-update-change.md +101 -0
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/SKILL.md +91 -23
- package/src/templates/mate-skills/claude/{mate-artifact-publish → mate-publish-artifact}/references/openspec.md +4 -3
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +74 -16
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +74 -68
- package/src/templates/mate-skills/openspec-references.ts +140 -0
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -2
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3727 -320
- package/src/tools/setup/capabilities/context-mode.ts +25 -5
- package/src/tools/setup/capabilities/graphify-gate.ts +31 -0
- package/src/tools/setup/capabilities/graphify-secrets.ts +255 -0
- package/src/tools/setup/capabilities/graphify.ts +12 -0
- package/src/tools/setup/capabilities/openspec.ts +273 -270
- package/src/tools/setup/capabilities/tokensave.ts +36 -3
- package/src/tools/setup/context-services.ts +9 -17
- package/src/tools/setup/dynamic-plugins/audiences.ts +7 -39
- package/src/tools/setup/dynamic-plugins/declarations.ts +1 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +4 -7
- package/src/tools/setup/dynamic-plugins/install.ts +24 -28
- package/src/tools/setup/engine.ts +93 -148
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +37 -17
- package/src/tools/setup/plugins/gitignore.ts +1 -7
- package/src/tools/setup/plugins/guidance.ts +7 -4
- package/src/tools/setup/providers/agent-file-sections.ts +4 -2
- package/src/tools/setup/providers/claude-attribution.ts +65 -0
- package/src/tools/setup/providers/claude-plugin-folders.ts +625 -0
- package/src/tools/setup/providers/claude-plugin-hooks.ts +39 -19
- package/src/tools/setup/providers/claude.ts +285 -34
- package/src/tools/setup/providers/opencode.ts +180 -11
- package/src/tools/setup/registry.ts +2 -24
- package/src/tools/setup/surface-target.ts +4 -13
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +131 -18
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/opencode-plugin/src/audience-layer.ts +0 -91
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/runtime/audience-layer.ts +0 -84
- package/src/runtime/opencode-audience-layer.ts +0 -176
- package/src/tools/setup/audience-isolation.fixture.ts +0 -325
- package/src/tools/setup/audience-layer.ts +0 -207
- package/src/tools/setup/audience-registrations.ts +0 -52
- package/src/tools/setup/contribution-ledger.ts +0 -207
- package/src/tools/setup/isolation-diagnostics.ts +0 -139
- package/src/tools/setup/runtime-documents.ts +0 -291
- 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 {
|
|
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
|
-
|
|
25
|
-
type
|
|
26
|
-
} from "../../tools/setup
|
|
31
|
+
inspectClaudePluginDirectories,
|
|
32
|
+
type ClaudePluginDirectoryInspection,
|
|
33
|
+
} from "../../tools/setup";
|
|
27
34
|
import { getRequiredPluginDrift } from "../../tools/setup/policy";
|
|
28
|
-
import
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
|
|
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) => [
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
["
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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 (
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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 (
|
|
54
|
-
process.
|
|
55
|
-
|
|
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 {
|
|
2
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
}
|