@uniqbit/mate-core 0.18.5 → 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/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 +177 -138
- 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 +176 -3
- 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/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 +5 -13
- 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 +4 -0
- 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 +9 -17
- 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 +48 -40
- package/src/lib/orchestrator/adapters/opencode.ts +2 -0
- 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 -137
- package/src/lib/orchestrator/projection-entries.ts +45 -14
- package/src/lib/orchestrator/projection-runtime-documents.ts +15 -6
- package/src/lib/orchestrator/projection-types.ts +3 -0
- package/src/lib/orchestrator/refresh-working-claude-links.ts +87 -0
- package/src/lib/orchestrator/session-prepare.ts +45 -5
- 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 -1
- 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/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 +15 -1
- package/src/runtime/projected-flag.ts +7 -0
- package/src/runtime/projected-guidance.ts +1 -0
- package/src/runtime/runtime-document-manifest.ts +62 -11
- 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 -2
- 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/{claude/mate-artifact-publish → agents/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/{agents/mate-artifact-publish → claude/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 +151 -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/install.ts +24 -28
- package/src/tools/setup/engine.ts +97 -21
- package/src/tools/setup/managed-output.ts +51 -0
- package/src/tools/setup/mate.ts +86 -11
- package/src/tools/setup/plugin.ts +35 -0
- 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 +284 -8
- package/src/tools/setup/providers/opencode.ts +180 -1
- package/src/tools/setup/utils.ts +14 -4
- package/src/tools/setup.ts +126 -10
- package/wrappers/bin/graphify +52 -4
- package/wrappers/bin/openspec +33 -9
- package/wrappers/lib/graphify-gate.mjs +8 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +0 -190
- package/src/tools/setup/runtime-documents.ts +0 -291
|
@@ -6,6 +6,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
6
6
|
|
|
7
7
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
8
8
|
import { gitEnvironment } from "../../runtime/companion-git";
|
|
9
|
+
import { companionClaudeMdHasOwnText } from "../../runtime/companion-instructions";
|
|
9
10
|
import { getActiveDistribution } from "../../distribution";
|
|
10
11
|
import { hasOpenspecCapability } from "../../lib/orchestrator/capabilities";
|
|
11
12
|
import { checkEngineRequirement } from "../../lib/orchestrator/engine-guard";
|
|
@@ -13,6 +14,8 @@ import type { GlobalConfigStore } from "../../lib/orchestrator/global-config-sto
|
|
|
13
14
|
import { pathIsDirectory } from "../../lib/fs-utils";
|
|
14
15
|
import { resolveRootContext, type RootContext } from "../../lib/orchestrator/root-context";
|
|
15
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";
|
|
16
19
|
import {
|
|
17
20
|
describeUnknownAudience,
|
|
18
21
|
readAudienceSelection,
|
|
@@ -23,7 +26,20 @@ import {
|
|
|
23
26
|
type PluginInspection,
|
|
24
27
|
type PluginVerificationFailure,
|
|
25
28
|
} from "../../tools/setup/dynamic-plugins/verify";
|
|
29
|
+
import { hydrateDynamicPlugins } from "../../tools/setup/dynamic-plugins/hydrate";
|
|
30
|
+
import {
|
|
31
|
+
inspectClaudePluginDirectories,
|
|
32
|
+
type ClaudePluginDirectoryInspection,
|
|
33
|
+
} from "../../tools/setup";
|
|
26
34
|
import { getRequiredPluginDrift } from "../../tools/setup/policy";
|
|
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";
|
|
27
43
|
import { resolveCommandOnPath } from "../../tools/setup/utils";
|
|
28
44
|
import { writeJsonStdout } from "../write-json-stdout";
|
|
29
45
|
import { printSection, renderKeyValueTable, renderTable } from "./status";
|
|
@@ -35,6 +51,12 @@ interface DoctorDeps {
|
|
|
35
51
|
/** Returns a member checkout's HEAD commit, or null when unreadable. */
|
|
36
52
|
gitHead?: (memberPath: string) => string | null;
|
|
37
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>;
|
|
38
60
|
/** Environment `MATE_AUDIENCE` is read from; defaults to the process environment. */
|
|
39
61
|
env?: Record<string, string | undefined>;
|
|
40
62
|
}
|
|
@@ -46,6 +68,8 @@ export interface DoctorToolInstallation {
|
|
|
46
68
|
source: string;
|
|
47
69
|
status: "ok" | "missing";
|
|
48
70
|
path: string | null;
|
|
71
|
+
/** Set for a tool Mate ships itself, such as the OpenSpec CLI. */
|
|
72
|
+
version?: string;
|
|
49
73
|
}
|
|
50
74
|
|
|
51
75
|
export interface DoctorHubMember {
|
|
@@ -79,6 +103,17 @@ export interface DoctorReport {
|
|
|
79
103
|
engineRequirement?: { range: string; ok: boolean; detail: string };
|
|
80
104
|
/** Present when the companion's `openspec/config.yaml` has `rules` keys OpenSpec ignores. */
|
|
81
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[];
|
|
82
117
|
hub?: { members: DoctorHubMember[] };
|
|
83
118
|
resolutionFailures: Array<{ companionPath: string; message: string }>;
|
|
84
119
|
}
|
|
@@ -87,6 +122,8 @@ interface ToolCheck {
|
|
|
87
122
|
name: string;
|
|
88
123
|
command: string;
|
|
89
124
|
source: string;
|
|
125
|
+
/** Locates a tool Mate ships instead of looking it up on PATH. */
|
|
126
|
+
bundled?: () => OpenSpecLocation;
|
|
90
127
|
}
|
|
91
128
|
|
|
92
129
|
/** Doctor's view of the shared root kind: registry/env resolution means cwd is a working repo. */
|
|
@@ -127,7 +164,12 @@ function getSelectedToolChecks(config: {
|
|
|
127
164
|
checks.push({ name: "uv", command: "uv", source: "package manager" });
|
|
128
165
|
}
|
|
129
166
|
if (selectedCapabilities.has("openspec")) {
|
|
130
|
-
checks.push({
|
|
167
|
+
checks.push({
|
|
168
|
+
name: "openspec",
|
|
169
|
+
command: "openspec",
|
|
170
|
+
source: "capability",
|
|
171
|
+
bundled: locateOpenSpec,
|
|
172
|
+
});
|
|
131
173
|
}
|
|
132
174
|
if (selectedCapabilities.has("tokensave")) {
|
|
133
175
|
checks.push({ name: "tokensave", command: "tokensave", source: "capability" });
|
|
@@ -147,6 +189,21 @@ function collectToolInstallations(
|
|
|
147
189
|
pathValue: string,
|
|
148
190
|
): DoctorToolInstallation[] {
|
|
149
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
|
+
}
|
|
150
207
|
const resolved = resolveCommandOnPath(check.command, pathValue);
|
|
151
208
|
return {
|
|
152
209
|
tool: check.name,
|
|
@@ -309,6 +366,19 @@ async function collectOpenSpecRuleWarnings(
|
|
|
309
366
|
}));
|
|
310
367
|
}
|
|
311
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
|
+
|
|
312
382
|
export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<DoctorReport> {
|
|
313
383
|
const cwd = path.resolve(deps.cwd ?? process.cwd());
|
|
314
384
|
const pathValue = deps.pathValue ?? process.env.PATH ?? "";
|
|
@@ -356,6 +426,23 @@ export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<Doctor
|
|
|
356
426
|
if (warnings.length > 0) report.openspecRuleWarnings = warnings;
|
|
357
427
|
}
|
|
358
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;
|
|
444
|
+
}
|
|
445
|
+
|
|
359
446
|
if (root.rootPath && root.config) {
|
|
360
447
|
const inspect =
|
|
361
448
|
deps.inspectPlugins ?? ((companionPath) => inspectDeclaredPlugins(companionPath, { env }));
|
|
@@ -386,7 +473,12 @@ function renderToolInstallations(installations: DoctorToolInstallation[]): strin
|
|
|
386
473
|
}
|
|
387
474
|
return renderTable(
|
|
388
475
|
["Tool", "Source", "Status", "Path"],
|
|
389
|
-
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
|
+
]),
|
|
390
482
|
);
|
|
391
483
|
}
|
|
392
484
|
|
|
@@ -404,6 +496,65 @@ function renderHubMembers(members: DoctorHubMember[]): string {
|
|
|
404
496
|
);
|
|
405
497
|
}
|
|
406
498
|
|
|
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(
|
|
533
|
+
renderTable(
|
|
534
|
+
["Capability", "Plugin Folder", "Status", "Plain Sessions"],
|
|
535
|
+
plugins.map((plugin) => [
|
|
536
|
+
plugin.capability,
|
|
537
|
+
plugin.path,
|
|
538
|
+
claudePluginStatus(plugin),
|
|
539
|
+
claudePluginPlainSessions(plugin),
|
|
540
|
+
]),
|
|
541
|
+
),
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (failures) {
|
|
545
|
+
tables.push(
|
|
546
|
+
renderTable(
|
|
547
|
+
["Capability", "Error"],
|
|
548
|
+
failures.map((failure) => [failure.capability, failure.reason]),
|
|
549
|
+
),
|
|
550
|
+
);
|
|
551
|
+
}
|
|
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");
|
|
556
|
+
}
|
|
557
|
+
|
|
407
558
|
function renderHumanReport(report: DoctorReport): void {
|
|
408
559
|
console.log("Mate Doctor");
|
|
409
560
|
console.log("");
|
|
@@ -477,6 +628,12 @@ function renderHumanReport(report: DoctorReport): void {
|
|
|
477
628
|
if (report.engineRequirement) {
|
|
478
629
|
printSection("Version Requirement", report.engineRequirement.detail);
|
|
479
630
|
}
|
|
631
|
+
if (report.claudePlugins || report.claudePluginFailures) {
|
|
632
|
+
printSection(
|
|
633
|
+
"Claude Plugins",
|
|
634
|
+
renderClaudePlugins(report.claudePlugins, report.claudePluginFailures),
|
|
635
|
+
);
|
|
636
|
+
}
|
|
480
637
|
if (report.openspecRuleWarnings) {
|
|
481
638
|
printSection(
|
|
482
639
|
"OpenSpec Config",
|
|
@@ -486,6 +643,15 @@ function renderHumanReport(report: DoctorReport): void {
|
|
|
486
643
|
),
|
|
487
644
|
);
|
|
488
645
|
}
|
|
646
|
+
if (report.companionInstructionWarnings) {
|
|
647
|
+
printSection(
|
|
648
|
+
"Companion Instructions",
|
|
649
|
+
renderList(
|
|
650
|
+
report.companionInstructionWarnings.map((warning) => warning.message),
|
|
651
|
+
"",
|
|
652
|
+
),
|
|
653
|
+
);
|
|
654
|
+
}
|
|
489
655
|
|
|
490
656
|
printSection(
|
|
491
657
|
"Resolution Failures",
|
|
@@ -506,7 +672,8 @@ function renderHumanReport(report: DoctorReport): void {
|
|
|
506
672
|
* capabilities and tool checks for companions, member health for hubs.
|
|
507
673
|
* Declared plugins of the effective set (base plus the `MATE_AUDIENCE`
|
|
508
674
|
* audience) are verified strictly (installed, loadable); any failure is named
|
|
509
|
-
* 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.
|
|
510
677
|
* `--json` emits the same report as one JSON document.
|
|
511
678
|
*/
|
|
512
679
|
export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {}): Promise<void> {
|
|
@@ -518,6 +685,12 @@ export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {
|
|
|
518
685
|
for (const failure of failures) {
|
|
519
686
|
process.stderr.write(`${FRAMEWORK_NAME}: plugin ${failure.package}: ${failure.reason}\n`);
|
|
520
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
|
+
}
|
|
521
694
|
if (report.audienceError) {
|
|
522
695
|
process.stderr.write(`${FRAMEWORK_NAME}: ${report.audienceError}\n`);
|
|
523
696
|
process.exitCode = 1;
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { highlightInline } from "./highlight";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safe Markdown subset for report text and captions: paragraphs, headings shifted
|
|
5
|
+
* below the section heading, one-level lists, code spans, strong, emphasis,
|
|
6
|
+
* `http(s)` links, and highlighted fenced code. Every literal is escaped before
|
|
7
|
+
* markup is added, so raw HTML and any other syntax degrade to visible text.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function escapeHTML(value: string): string {
|
|
11
|
+
return value.replace(/[&<>"']/g, (character) => {
|
|
12
|
+
switch (character) {
|
|
13
|
+
case "&":
|
|
14
|
+
return "&";
|
|
15
|
+
case "<":
|
|
16
|
+
return "<";
|
|
17
|
+
case ">":
|
|
18
|
+
return ">";
|
|
19
|
+
case '"':
|
|
20
|
+
return """;
|
|
21
|
+
default:
|
|
22
|
+
return "'";
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FENCE = /^\s{0,3}```\s*([\w+#.-]*)\s*$/;
|
|
28
|
+
const HEADING = /^\s{0,3}(#{1,6})\s+(.*)$/;
|
|
29
|
+
const LIST_ITEM = /^\s{0,3}([-*]|\d+[.)])\s+(.*)$/;
|
|
30
|
+
const INLINE_TOKEN = /`([^`\n]+)`|\[([^\]\n]+)\]\(([^()\s]+)\)/g;
|
|
31
|
+
const SAFE_URL = /^https?:\/\/\S+$/i;
|
|
32
|
+
|
|
33
|
+
/** Emphasis runs over already-escaped text, which carries no `*` or `_` of its own. */
|
|
34
|
+
function emphasize(escaped: string): string {
|
|
35
|
+
return escaped
|
|
36
|
+
.replace(/\*\*(?=\S)([^*]*?\S)\*\*/g, "<strong>$1</strong>")
|
|
37
|
+
.replace(/\*(?=\S)([^*]*?\S)\*/g, "<em>$1</em>")
|
|
38
|
+
.replace(/(^|\W)_(?=\S)([^_]*?\S)_(?!\w)/g, "$1<em>$2</em>");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderInline(source: string): string {
|
|
42
|
+
let html = "";
|
|
43
|
+
let last = 0;
|
|
44
|
+
for (const match of source.matchAll(INLINE_TOKEN)) {
|
|
45
|
+
html += emphasize(escapeHTML(source.slice(last, match.index)));
|
|
46
|
+
const [whole, code, label, url] = match;
|
|
47
|
+
if (code !== undefined) {
|
|
48
|
+
html += `<code>${escapeHTML(code)}</code>`;
|
|
49
|
+
} else if (SAFE_URL.test(url)) {
|
|
50
|
+
html += `<a href="${escapeHTML(url)}" rel="noopener noreferrer">${emphasize(escapeHTML(label))}</a>`;
|
|
51
|
+
} else {
|
|
52
|
+
html += emphasize(escapeHTML(whole));
|
|
53
|
+
}
|
|
54
|
+
last = match.index + whole.length;
|
|
55
|
+
}
|
|
56
|
+
return html + emphasize(escapeHTML(source.slice(last)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderFence(code: string[], language: string): string {
|
|
60
|
+
return `<pre class="report-code">${highlightInline(code.join("\n"), language || undefined)}</pre>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function renderMarkdownSubset(source: string): string {
|
|
64
|
+
const lines = source.replace(/\r\n?/g, "\n").split("\n");
|
|
65
|
+
const blocks: string[] = [];
|
|
66
|
+
let paragraph: string[] = [];
|
|
67
|
+
let list: { tag: "ul" | "ol"; items: string[] } | undefined;
|
|
68
|
+
|
|
69
|
+
const flush = (): void => {
|
|
70
|
+
if (paragraph.length) blocks.push(`<p>${paragraph.map(renderInline).join("\n")}</p>`);
|
|
71
|
+
if (list) blocks.push(`<${list.tag}>${list.items.join("")}</${list.tag}>`);
|
|
72
|
+
paragraph = [];
|
|
73
|
+
list = undefined;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
for (let index = 0; index < lines.length; index++) {
|
|
77
|
+
const line = lines[index];
|
|
78
|
+
const fence = FENCE.exec(line);
|
|
79
|
+
if (fence) {
|
|
80
|
+
flush();
|
|
81
|
+
const code: string[] = [];
|
|
82
|
+
while (++index < lines.length && !FENCE.test(lines[index])) code.push(lines[index]);
|
|
83
|
+
blocks.push(renderFence(code, fence[1]));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (line.trim() === "") {
|
|
87
|
+
flush();
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const heading = HEADING.exec(line);
|
|
91
|
+
if (heading) {
|
|
92
|
+
flush();
|
|
93
|
+
const level = Math.min(heading[1].length + 2, 6);
|
|
94
|
+
blocks.push(`<h${level}>${renderInline(heading[2].trim())}</h${level}>`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const item = LIST_ITEM.exec(line);
|
|
98
|
+
if (item) {
|
|
99
|
+
const tag = /\d/.test(item[1]) ? "ol" : "ul";
|
|
100
|
+
if (paragraph.length || (list && list.tag !== tag)) flush();
|
|
101
|
+
list ??= { tag, items: [] };
|
|
102
|
+
list.items.push(`<li>${renderInline(item[2])}</li>`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (list) flush();
|
|
106
|
+
paragraph.push(line);
|
|
107
|
+
}
|
|
108
|
+
flush();
|
|
109
|
+
return blocks.join("");
|
|
110
|
+
}
|