@uniqbit/mate-core 0.15.5 → 0.16.0-canary.1
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/.claude-plugin/plugin.json +2 -2
- package/claude-plugin/hooks/hooks.json +3 -6
- package/claude-plugin/hooks/session-guidance.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +19 -0
- package/package.json +7 -5
- package/src/cli/commands/artifact/artifact.ts +19 -3
- package/src/cli/commands/artifact/finish/command.ts +183 -57
- package/src/cli/commands/artifact/finish/engine.ts +80 -91
- package/src/cli/commands/artifact/finish/finisher.ts +26 -33
- package/src/cli/commands/artifact/finish/git.ts +34 -23
- package/src/cli/commands/artifact/finish/index.ts +9 -3
- package/src/cli/commands/artifact/finish/openspec.ts +225 -97
- package/src/cli/commands/artifact/pending/command.ts +175 -0
- package/src/cli/commands/artifact/pending/discovery.ts +249 -0
- package/src/cli/commands/artifact/pending/index.ts +17 -0
- package/src/cli/commands/cap/index-cmd.ts +9 -1
- package/src/cli/commands/cap/index.ts +2 -6
- package/src/cli/commands/cap/tokensave.ts +4 -4
- package/src/cli/commands/companion/companion.ts +5 -1
- package/src/cli/commands/companion/link.ts +2 -2
- package/src/cli/commands/companion/sync.ts +92 -0
- package/src/cli/commands/doctor.ts +0 -3
- package/src/cli/commands/launch/shared.ts +23 -5
- package/src/cli/commands/report/collector.ts +72 -78
- package/src/cli/commands/report/contract.ts +40 -1
- package/src/cli/commands/report/highlight.ts +27 -0
- package/src/cli/commands/report/index.ts +11 -18
- package/src/cli/commands/report/renderer.ts +199 -2
- package/src/cli/commands/report/types.ts +26 -1
- package/src/cli/commands/shared/companion-selection.ts +107 -10
- package/src/cli/commands/studio/areas.ts +68 -0
- package/src/cli/commands/studio/index.ts +69 -0
- package/src/cli/commands/studio/inventory.ts +55 -0
- package/src/cli/commands/studio/mate-inventory.ts +43 -0
- package/src/cli/commands/studio/openspec-cli.ts +198 -0
- package/src/cli/commands/studio/payload.ts +184 -0
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +61 -0
- package/src/cli/commands/studio/server.ts +201 -0
- package/src/cli/commands/studio/snapshot.ts +63 -0
- package/src/cli/commands/studio/topology.ts +199 -0
- package/src/cli/commands/studio/views/client.ts +197 -0
- package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
- package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
- package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
- package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
- package/src/cli/commands/studio/views/document.tsx +256 -0
- package/src/cli/commands/studio/views/error.tsx +20 -0
- package/src/cli/commands/studio/views/model.ts +34 -0
- package/src/cli/commands/studio/views/pairings.tsx +38 -0
- package/src/cli/commands/studio/views/skills/index.tsx +87 -0
- package/src/cli/commands/studio/views/specs/index.tsx +101 -0
- package/src/cli/commands/studio/views/styles.ts +389 -0
- package/src/cli/commands/studio/views/warnings.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
- package/src/cli/commands/unwrap.ts +70 -0
- package/src/cli/commands/wrap.ts +164 -0
- package/src/cli/main.ts +68 -19
- package/src/cli/parse-flags.ts +36 -11
- package/src/cli/usage.ts +12 -3
- package/src/framework.ts +1 -7
- package/src/hooks/session-banner.ts +64 -11
- package/src/hooks/session-guidance.ts +40 -0
- package/src/hooks/validate-artifact-path.ts +108 -35
- package/src/lib/fs-utils.ts +9 -0
- package/src/lib/install.ts +33 -0
- package/src/lib/orchestrator/adapters/base.ts +14 -125
- package/src/lib/orchestrator/adapters/claude.ts +0 -11
- package/src/lib/orchestrator/adapters/opencode.ts +2 -32
- package/src/lib/orchestrator/companion-git-sync.ts +94 -84
- package/src/lib/orchestrator/config-store.ts +2 -21
- package/src/lib/orchestrator/editor.ts +12 -22
- package/src/lib/orchestrator/framework-context.ts +17 -6
- package/src/lib/orchestrator/global-config-store.ts +1 -1
- package/src/lib/orchestrator/launcher.ts +97 -7
- package/src/lib/orchestrator/opencode-guidance.ts +4 -56
- package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
- package/src/lib/orchestrator/projection-companion-link.ts +62 -0
- package/src/lib/orchestrator/projection-entries.ts +377 -0
- package/src/lib/orchestrator/projection-record.ts +56 -0
- package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
- package/src/lib/orchestrator/projection-types.ts +169 -0
- package/src/lib/orchestrator/repo-local-registry.ts +37 -133
- package/src/lib/orchestrator/repo-local-store.ts +96 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
- package/src/lib/orchestrator/types.ts +1 -0
- package/src/lib/orchestrator/working-repo-projection.ts +366 -0
- package/src/lib/orchestrator/workspace-inventory.ts +1 -1
- package/src/lib/package-paths.ts +11 -1
- package/src/lib/public-npm.ts +2 -1
- package/src/lib/update-checker.ts +15 -9
- package/src/opencode/companion-hooks.ts +89 -245
- package/src/opencode/companion-policy.ts +35 -10
- package/src/opencode/index.ts +1 -0
- package/src/opencode/projected-guidance.ts +56 -0
- package/src/opencode/tui.tsx +13 -4
- package/src/playbooks/companion-guidance.ts +32 -116
- package/src/plugins.ts +0 -1
- package/src/runtime/companion-git-state.ts +156 -0
- package/src/runtime/companion-git.ts +203 -0
- package/src/runtime/companion-guidance.ts +222 -0
- package/src/runtime/companion-sync.ts +298 -0
- package/src/runtime/env-names.ts +30 -0
- package/src/runtime/env.ts +67 -35
- package/src/runtime/framework.ts +10 -0
- package/src/runtime/freshness.ts +58 -0
- package/src/runtime/index.ts +104 -0
- package/src/runtime/install.ts +30 -0
- package/src/runtime/policy.ts +66 -0
- package/src/runtime/projected-guidance.ts +45 -0
- package/src/runtime/projection.ts +224 -0
- package/src/runtime/repo-local.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
- package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +156 -0
- package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +156 -0
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/report-assets/README.md +32 -0
- package/src/templates/report-assets/mermaid.LICENSE +21 -0
- package/src/templates/report-assets/mermaid.min.js +4376 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
- package/src/tools/setup/capabilities/graphify.ts +16 -7
- package/src/tools/setup/capabilities/openspec.ts +63 -56
- package/src/tools/setup/capabilities/tokensave.ts +116 -2
- package/src/tools/setup/engine.ts +34 -6
- package/src/tools/setup/mate.ts +42 -13
- package/src/tools/setup/plugin.ts +9 -0
- package/src/tools/setup/plugins/guidance.ts +11 -1
- package/src/tools/setup/providers/claude-format.ts +49 -4
- package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
- package/src/tools/setup/providers/claude.ts +55 -220
- package/src/tools/setup/providers/opencode.ts +41 -14
- package/src/tools/setup/runtime-documents.ts +174 -0
- package/src/tools/setup/surface-target.ts +50 -0
- package/src/tools/setup/working-repo-cleanup.ts +33 -26
- package/src/tools/setup/working-repo-local-state.ts +21 -1
- package/src/tools/setup.ts +25 -3
- package/wrappers/bin/graphify +57 -8
- package/wrappers/bin/openspec +50 -3
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
- package/src/cli/commands/cap/headroom.ts +0 -52
- package/src/cli/commands/workspace/list.ts +0 -25
- package/src/cli/commands/workspace/materialize.ts +0 -46
- package/src/cli/commands/workspace/workspace.ts +0 -22
- package/src/hooks/artifact-finish-nudge.ts +0 -244
- package/src/lib/orchestrator/headroom/proxy.ts +0 -116
- package/src/lib/orchestrator/workspace-materialize.ts +0 -80
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
- package/src/tools/setup/capabilities/headroom.ts +0 -57
- /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
|
@@ -4,6 +4,7 @@ import type { SavingsEntry, SpendingEntry, ToolStatus } from "./types";
|
|
|
4
4
|
|
|
5
5
|
export interface CollectorDeps {
|
|
6
6
|
spawn?: typeof spawnSync;
|
|
7
|
+
now?: () => Date;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
function spawnCmd(cmd: string, args: string[], opts: { cwd: string }, deps: CollectorDeps) {
|
|
@@ -21,14 +22,28 @@ function parseJson<T>(output: string): T | null {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function reportSince(days: number, deps: CollectorDeps): string {
|
|
26
|
+
const since = new Date(deps.now?.() ?? new Date());
|
|
27
|
+
since.setDate(since.getDate() - days);
|
|
28
|
+
return since.toISOString().slice(0, 10);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function utcDateFromEpochSeconds(value: unknown): string | null {
|
|
32
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
33
|
+
const date = new Date(value * 1000);
|
|
34
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString().slice(0, 10);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function finiteNumber(value: unknown): number {
|
|
38
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
// ccusage JSON: { daily: [{ period, totalCost, totalTokens, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, modelBreakdowns: [{ modelName, cost, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens }] }], totals: { totalCost, totalTokens, ... } }
|
|
25
42
|
export async function collectCcusageSpending(
|
|
26
43
|
days: number,
|
|
27
44
|
deps: CollectorDeps = {},
|
|
28
45
|
): Promise<{ entries: SpendingEntry[]; status: ToolStatus; friendlyError?: string }> {
|
|
29
|
-
const
|
|
30
|
-
since.setDate(since.getDate() - days);
|
|
31
|
-
const sinceStr = since.toISOString().slice(0, 10);
|
|
46
|
+
const sinceStr = reportSince(days, deps);
|
|
32
47
|
|
|
33
48
|
// Try bunx first, then npx
|
|
34
49
|
const runners = [
|
|
@@ -96,7 +111,7 @@ export async function collectCcusageSpending(
|
|
|
96
111
|
};
|
|
97
112
|
}
|
|
98
113
|
|
|
99
|
-
|
|
114
|
+
/** tokensave gain --history JSON: [{ day, saved_tokens, calls, usd }] */
|
|
100
115
|
export async function collectTokenSaveSavings(
|
|
101
116
|
repoPath: string,
|
|
102
117
|
days: number,
|
|
@@ -104,7 +119,7 @@ export async function collectTokenSaveSavings(
|
|
|
104
119
|
): Promise<{ entry: SavingsEntry | null; status: ToolStatus }> {
|
|
105
120
|
const result = spawnCmd(
|
|
106
121
|
"tokensave",
|
|
107
|
-
["gain", "--json", "--range",
|
|
122
|
+
["gain", "--history", "--json", "--range", "all"],
|
|
108
123
|
{ cwd: repoPath },
|
|
109
124
|
deps,
|
|
110
125
|
);
|
|
@@ -116,37 +131,58 @@ export async function collectTokenSaveSavings(
|
|
|
116
131
|
};
|
|
117
132
|
}
|
|
118
133
|
|
|
119
|
-
const data = parseJson<
|
|
120
|
-
|
|
134
|
+
const data = parseJson<
|
|
135
|
+
Array<{ day?: number; saved_tokens?: number; calls?: number; usd?: number } | null>
|
|
136
|
+
>(result.stdout);
|
|
137
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
121
138
|
return {
|
|
122
139
|
entry: null,
|
|
123
140
|
status: { name: "tokensave", enabled: false, status: "no data" },
|
|
124
141
|
};
|
|
125
142
|
}
|
|
126
143
|
|
|
144
|
+
const sinceStr = reportSince(days, deps);
|
|
145
|
+
let matchingDays = 0;
|
|
146
|
+
let tokensSaved = 0;
|
|
147
|
+
let calls = 0;
|
|
148
|
+
let costSaved = 0;
|
|
149
|
+
for (const day of data) {
|
|
150
|
+
const date = utcDateFromEpochSeconds(day?.day);
|
|
151
|
+
if (!date || date < sinceStr) continue;
|
|
152
|
+
matchingDays++;
|
|
153
|
+
tokensSaved += finiteNumber(day?.saved_tokens);
|
|
154
|
+
calls += finiteNumber(day?.calls);
|
|
155
|
+
costSaved += finiteNumber(day?.usd);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (matchingDays === 0) {
|
|
159
|
+
return {
|
|
160
|
+
entry: null,
|
|
161
|
+
status: { name: "tokensave", enabled: false, status: "no window data" },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
127
165
|
return {
|
|
128
166
|
entry: {
|
|
129
167
|
tool: "tokensave",
|
|
130
|
-
tokensSaved
|
|
131
|
-
calls
|
|
132
|
-
costSaved
|
|
133
|
-
efficiency:
|
|
134
|
-
(data.calls ?? 0) > 0
|
|
135
|
-
? `${formatEfficiency(data.saved_tokens ?? 0, data.calls ?? 0)}`
|
|
136
|
-
: "N/A",
|
|
168
|
+
tokensSaved,
|
|
169
|
+
calls,
|
|
170
|
+
costSaved,
|
|
171
|
+
efficiency: calls > 0 ? formatEfficiency(tokensSaved, calls) : "N/A",
|
|
137
172
|
},
|
|
138
173
|
status: { name: "tokensave", enabled: true, status: "ok" },
|
|
139
174
|
};
|
|
140
175
|
}
|
|
141
176
|
|
|
142
|
-
|
|
177
|
+
/** rtk gain --daily JSON: { daily: [{ date, saved_tokens, commands }] } */
|
|
143
178
|
export async function collectRTKSavings(
|
|
144
179
|
repoPath: string,
|
|
180
|
+
days: number,
|
|
145
181
|
deps: CollectorDeps = {},
|
|
146
182
|
): Promise<{ entry: SavingsEntry | null; status: ToolStatus }> {
|
|
147
183
|
const result = spawnCmd(
|
|
148
184
|
"rtk",
|
|
149
|
-
["gain", "--project", "--format", "json"],
|
|
185
|
+
["gain", "--project", "--daily", "--format", "json"],
|
|
150
186
|
{ cwd: repoPath },
|
|
151
187
|
deps,
|
|
152
188
|
);
|
|
@@ -159,81 +195,42 @@ export async function collectRTKSavings(
|
|
|
159
195
|
}
|
|
160
196
|
|
|
161
197
|
const data = parseJson<{
|
|
162
|
-
|
|
198
|
+
daily?: Array<{ date?: string; saved_tokens?: number; commands?: number } | null>;
|
|
163
199
|
}>(result.stdout);
|
|
164
|
-
if (!data || !data.
|
|
200
|
+
if (!data || !Array.isArray(data.daily) || data.daily.length === 0) {
|
|
165
201
|
return {
|
|
166
202
|
entry: null,
|
|
167
203
|
status: { name: "rtk", enabled: false, status: "no data" },
|
|
168
204
|
};
|
|
169
205
|
}
|
|
170
206
|
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
? `${formatEfficiency(s.total_saved ?? 0, s.total_commands ?? 0)}`
|
|
181
|
-
: "N/A",
|
|
182
|
-
},
|
|
183
|
-
status: { name: "rtk", enabled: true, status: "ok" },
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// headroom savings JSON: { windows: { last_7_days: { tokens_saved, cost_usd, calls } } }
|
|
188
|
-
export async function collectHeadroomSavings(
|
|
189
|
-
days: number,
|
|
190
|
-
deps: CollectorDeps = {},
|
|
191
|
-
): Promise<{ entry: SavingsEntry | null; status: ToolStatus }> {
|
|
192
|
-
const result = spawnCmd(
|
|
193
|
-
"headroom",
|
|
194
|
-
["savings", "--json", "--days", String(days)],
|
|
195
|
-
{ cwd: process.cwd() },
|
|
196
|
-
deps,
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
if (result.error || result.status !== 0) {
|
|
200
|
-
return {
|
|
201
|
-
entry: null,
|
|
202
|
-
status: { name: "headroom", enabled: false, status: "not enabled" },
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const data = parseJson<{
|
|
207
|
-
windows?: Record<string, { tokens_saved?: number; cost_usd?: number; calls?: number }>;
|
|
208
|
-
}>(result.stdout);
|
|
209
|
-
if (!data || !data.windows) {
|
|
210
|
-
return {
|
|
211
|
-
entry: null,
|
|
212
|
-
status: { name: "headroom", enabled: false, status: "no data" },
|
|
213
|
-
};
|
|
207
|
+
const sinceStr = reportSince(days, deps);
|
|
208
|
+
let matchingDays = 0;
|
|
209
|
+
let tokensSaved = 0;
|
|
210
|
+
let calls = 0;
|
|
211
|
+
for (const day of data.daily) {
|
|
212
|
+
if (typeof day?.date !== "string" || day.date < sinceStr) continue;
|
|
213
|
+
matchingDays++;
|
|
214
|
+
tokensSaved += finiteNumber(day?.saved_tokens);
|
|
215
|
+
calls += finiteNumber(day?.commands);
|
|
214
216
|
}
|
|
215
217
|
|
|
216
|
-
|
|
217
|
-
const window = data.windows[key] ?? data.windows["last_7_days"] ?? data.windows["all_time"];
|
|
218
|
-
if (!window) {
|
|
218
|
+
if (matchingDays === 0) {
|
|
219
219
|
return {
|
|
220
220
|
entry: null,
|
|
221
|
-
status: { name: "
|
|
221
|
+
status: { name: "rtk", enabled: false, status: "no window data" },
|
|
222
222
|
};
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
return {
|
|
226
226
|
entry: {
|
|
227
|
-
tool: "
|
|
228
|
-
tokensSaved
|
|
229
|
-
calls
|
|
230
|
-
costSaved:
|
|
231
|
-
efficiency:
|
|
232
|
-
(window.calls ?? 0) > 0
|
|
233
|
-
? `${formatEfficiency(window.tokens_saved ?? 0, window.calls ?? 0)}`
|
|
234
|
-
: "N/A",
|
|
227
|
+
tool: "rtk",
|
|
228
|
+
tokensSaved,
|
|
229
|
+
calls,
|
|
230
|
+
costSaved: 0,
|
|
231
|
+
efficiency: calls > 0 ? formatEfficiency(tokensSaved, calls) : "N/A",
|
|
235
232
|
},
|
|
236
|
-
status: { name: "
|
|
233
|
+
status: { name: "rtk", enabled: true, status: "ok" },
|
|
237
234
|
};
|
|
238
235
|
}
|
|
239
236
|
|
|
@@ -253,7 +250,6 @@ export function mergeResults(
|
|
|
253
250
|
tokensaveWorking: SavingsEntry | null,
|
|
254
251
|
tokensaveCompanion: SavingsEntry | null,
|
|
255
252
|
rtkEntry: SavingsEntry | null,
|
|
256
|
-
headroomEntry: SavingsEntry | null,
|
|
257
253
|
): {
|
|
258
254
|
spending: SpendingEntry[];
|
|
259
255
|
savings: SavingsEntry[];
|
|
@@ -279,9 +275,7 @@ export function mergeResults(
|
|
|
279
275
|
};
|
|
280
276
|
}
|
|
281
277
|
|
|
282
|
-
const savings: SavingsEntry[] = [mergedTokensave, rtkEntry
|
|
283
|
-
Boolean,
|
|
284
|
-
) as SavingsEntry[];
|
|
278
|
+
const savings: SavingsEntry[] = [mergedTokensave, rtkEntry].filter(Boolean) as SavingsEntry[];
|
|
285
279
|
const totalSavings = savings.reduce((sum, e) => sum + e.costSaved, 0);
|
|
286
280
|
|
|
287
281
|
return {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
REPORT_DOCUMENT_VERSION,
|
|
3
|
+
type ReportDiagramSection,
|
|
4
|
+
type ReportDiffSection,
|
|
3
5
|
type ReportDocument,
|
|
4
6
|
type ReportKeyValue,
|
|
5
7
|
type ReportMetric,
|
|
@@ -114,6 +116,32 @@ function validateStatuses(value: unknown, path: string, issues: ReportValidation
|
|
|
114
116
|
return true;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
const DIAGRAM_PAYLOADS = ["mermaid", "text"] as const;
|
|
120
|
+
|
|
121
|
+
/** A diagram carries exactly one payload; an empty or non-string payload counts as absent. */
|
|
122
|
+
function validateDiagram(
|
|
123
|
+
value: Record<string, unknown>,
|
|
124
|
+
path: string,
|
|
125
|
+
issues: ReportValidationIssue[],
|
|
126
|
+
): void {
|
|
127
|
+
const supplied = DIAGRAM_PAYLOADS.filter((key) => value[key] !== undefined);
|
|
128
|
+
for (const key of supplied) {
|
|
129
|
+
if (typeof value[key] !== "string" || (value[key] as string).trim() === "") {
|
|
130
|
+
issues.push({ path: `${path}.${key}`, message: "must be a non-empty string" });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (supplied.length === 1) return;
|
|
134
|
+
|
|
135
|
+
const name = typeof value.id === "string" && value.id.trim() !== "" ? value.id : path;
|
|
136
|
+
issues.push({
|
|
137
|
+
path,
|
|
138
|
+
message:
|
|
139
|
+
supplied.length === 0
|
|
140
|
+
? `section "${name}" must carry exactly one of mermaid or text, but carries neither`
|
|
141
|
+
: `section "${name}" must carry exactly one of mermaid or text, but carries both`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
117
145
|
function validateSection(value: unknown, path: string, issues: ReportValidationIssue[]): void {
|
|
118
146
|
if (!isRecord(value)) {
|
|
119
147
|
issues.push({ path, message: "must be an object" });
|
|
@@ -182,10 +210,19 @@ function validateSection(value: unknown, path: string, issues: ReportValidationI
|
|
|
182
210
|
issues.push({ path: `${path}.content`, message: "must be a string" });
|
|
183
211
|
}
|
|
184
212
|
return;
|
|
213
|
+
case "diagram":
|
|
214
|
+
validateDiagram(value, path, issues);
|
|
215
|
+
return;
|
|
216
|
+
case "diff":
|
|
217
|
+
if (typeof value.patch !== "string") {
|
|
218
|
+
issues.push({ path: `${path}.patch`, message: "must be a string" });
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
185
221
|
default:
|
|
186
222
|
issues.push({
|
|
187
223
|
path: `${path}.type`,
|
|
188
|
-
message:
|
|
224
|
+
message:
|
|
225
|
+
"must be one of metadata, metrics, key-value, table, statuses, text, diagram, or diff",
|
|
189
226
|
});
|
|
190
227
|
}
|
|
191
228
|
}
|
|
@@ -248,6 +285,8 @@ export function parseReportDocument(json: string): ReportDocument {
|
|
|
248
285
|
}
|
|
249
286
|
|
|
250
287
|
export type {
|
|
288
|
+
ReportDiagramSection,
|
|
289
|
+
ReportDiffSection,
|
|
251
290
|
ReportDocument,
|
|
252
291
|
ReportKeyValue,
|
|
253
292
|
ReportMetric,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { normalizeLanguage, renderNodesToHtml, renderTokens, tokenize } from "@tanstack/highlight";
|
|
2
|
+
import { createThemeCss } from "@tanstack/highlight/theme";
|
|
3
|
+
import githubLight from "@tanstack/highlight/themes/github-light";
|
|
4
|
+
|
|
5
|
+
/** Sole boundary against `@tanstack/highlight`, so an upstream API break stays a single-file fix. */
|
|
6
|
+
|
|
7
|
+
/** Language every unknown or unsupported candidate resolves to. */
|
|
8
|
+
export const PLAIN_LANGUAGE = "plaintext";
|
|
9
|
+
|
|
10
|
+
export function resolveHighlightLanguage(candidate: string | undefined): string {
|
|
11
|
+
return normalizeLanguage(candidate);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Class-annotated inline markup for one fragment, escaped and without a block wrapper.
|
|
16
|
+
* An unknown language yields plain escaped text carrying no token classes.
|
|
17
|
+
*/
|
|
18
|
+
export function highlightInline(code: string, language: string | undefined): string {
|
|
19
|
+
const resolved = resolveHighlightLanguage(language);
|
|
20
|
+
const { tokens } = tokenize(code, { lang: resolved });
|
|
21
|
+
return renderNodesToHtml(renderTokens(tokens));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Token colours as an inlinable stylesheet; carries no script, font, or external reference. */
|
|
25
|
+
export function highlightStyleBlock(codeBlockSelector: string): string {
|
|
26
|
+
return createThemeCss({ light: githubLight, codeBlockSelector });
|
|
27
|
+
}
|
|
@@ -4,7 +4,6 @@ import { resolveFrameworkContext } from "../../../lib/orchestrator/framework-con
|
|
|
4
4
|
import { ensureUnambiguousCompanion } from "../shared/companion-selection";
|
|
5
5
|
import {
|
|
6
6
|
collectCcusageSpending,
|
|
7
|
-
collectHeadroomSavings,
|
|
8
7
|
collectRTKSavings,
|
|
9
8
|
collectTokenSaveSavings,
|
|
10
9
|
mergeResults,
|
|
@@ -135,12 +134,16 @@ export async function runReportCommand(
|
|
|
135
134
|
.map((a) => a.trim())
|
|
136
135
|
.filter(Boolean)
|
|
137
136
|
: [];
|
|
137
|
+
const reportNow = new Date();
|
|
138
|
+
const collectorDeps: CollectorDeps = {
|
|
139
|
+
spawn: deps.spawn,
|
|
140
|
+
now: deps.now ?? (() => reportNow),
|
|
141
|
+
};
|
|
138
142
|
|
|
139
143
|
const tokensaveEnabled = enabledCapabilities.includes("tokensave");
|
|
140
|
-
const headroomEnabled = enabledCapabilities.includes("headroom");
|
|
141
144
|
const rtkEnabled = enabledCapabilities.includes("rtk");
|
|
142
145
|
|
|
143
|
-
const ccusageResult = await collectCcusageSpending(options.days,
|
|
146
|
+
const ccusageResult = await collectCcusageSpending(options.days, collectorDeps);
|
|
144
147
|
if (ccusageResult.friendlyError) console.warn(ccusageResult.friendlyError);
|
|
145
148
|
|
|
146
149
|
let tokensaveWorking: Awaited<ReturnType<typeof collectTokenSaveSavings>> = {
|
|
@@ -155,26 +158,21 @@ export async function runReportCommand(
|
|
|
155
158
|
entry: null,
|
|
156
159
|
status: { name: "rtk", enabled: false, status: "not enabled" },
|
|
157
160
|
};
|
|
158
|
-
let headroomResult: Awaited<ReturnType<typeof collectHeadroomSavings>> = {
|
|
159
|
-
entry: null,
|
|
160
|
-
status: { name: "headroom", enabled: false, status: "not enabled" },
|
|
161
|
-
};
|
|
162
161
|
|
|
163
162
|
if (tokensaveEnabled) {
|
|
164
163
|
[tokensaveWorking, tokensaveCompanion] = await Promise.all([
|
|
165
|
-
collectTokenSaveSavings(workingRepoPath, options.days,
|
|
166
|
-
collectTokenSaveSavings(companionPath, options.days,
|
|
164
|
+
collectTokenSaveSavings(workingRepoPath, options.days, collectorDeps),
|
|
165
|
+
collectTokenSaveSavings(companionPath, options.days, collectorDeps),
|
|
167
166
|
]);
|
|
168
167
|
}
|
|
169
|
-
if (rtkEnabled)
|
|
170
|
-
|
|
168
|
+
if (rtkEnabled)
|
|
169
|
+
rtkResult = await collectRTKSavings(workingRepoPath, options.days, collectorDeps);
|
|
171
170
|
|
|
172
171
|
const merged = mergeResults(
|
|
173
172
|
ccusageResult.entries,
|
|
174
173
|
tokensaveWorking.entry,
|
|
175
174
|
tokensaveCompanion.entry,
|
|
176
175
|
rtkResult.entry,
|
|
177
|
-
headroomResult.entry,
|
|
178
176
|
);
|
|
179
177
|
const reportData: ReportData = {
|
|
180
178
|
days: options.days,
|
|
@@ -185,12 +183,7 @@ export async function runReportCommand(
|
|
|
185
183
|
enabledCapabilities,
|
|
186
184
|
spending: merged.spending,
|
|
187
185
|
savings: merged.savings,
|
|
188
|
-
toolStatus: [
|
|
189
|
-
ccusageResult.status,
|
|
190
|
-
tokensaveWorking.status,
|
|
191
|
-
rtkResult.status,
|
|
192
|
-
headroomResult.status,
|
|
193
|
-
],
|
|
186
|
+
toolStatus: [ccusageResult.status, tokensaveWorking.status, rtkResult.status],
|
|
194
187
|
totalSpending: merged.totalSpending,
|
|
195
188
|
totalSavings: merged.totalSavings,
|
|
196
189
|
netSpend: merged.netSpend,
|
|
@@ -1,13 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
1
4
|
import { reportDataToDocument } from "./adapter";
|
|
2
5
|
import { validateReportDocument } from "./contract";
|
|
6
|
+
import { highlightInline, highlightStyleBlock } from "./highlight";
|
|
3
7
|
import type {
|
|
4
8
|
ReportData,
|
|
9
|
+
ReportDiagramSection,
|
|
10
|
+
ReportDiffSection,
|
|
5
11
|
ReportDocument,
|
|
6
12
|
ReportKeyValue,
|
|
7
13
|
ReportSection,
|
|
8
14
|
ReportValue,
|
|
9
15
|
} from "./types";
|
|
10
16
|
|
|
17
|
+
const MERMAID_RUNTIME_PATH = join(
|
|
18
|
+
import.meta.dirname,
|
|
19
|
+
"../../../templates/report-assets/mermaid.min.js",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const MERMAID_RUNTIME_ID = "mermaid-runtime";
|
|
23
|
+
|
|
24
|
+
const CODE_BLOCK_SELECTOR = "pre.report-code";
|
|
25
|
+
|
|
26
|
+
let mermaidRuntime: string | undefined;
|
|
27
|
+
|
|
28
|
+
/** A literal `</script` inside the bundle would close the inline element, so it is neutralised. */
|
|
29
|
+
function readMermaidRuntime(): string {
|
|
30
|
+
mermaidRuntime ??= readFileSync(MERMAID_RUNTIME_PATH, "utf8").replaceAll(
|
|
31
|
+
"</script",
|
|
32
|
+
String.raw`<\/script`,
|
|
33
|
+
);
|
|
34
|
+
return mermaidRuntime;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const carriesMermaid = (sections: ReportSection[]): boolean =>
|
|
38
|
+
sections.some((section) => section.type === "diagram" && section.mermaid !== undefined);
|
|
39
|
+
|
|
40
|
+
const carriesVisual = (sections: ReportSection[]): boolean =>
|
|
41
|
+
sections.some((section) => section.type === "diagram" || section.type === "diff");
|
|
42
|
+
|
|
11
43
|
type RenderInput = ReportDocument | ReportData;
|
|
12
44
|
|
|
13
45
|
function isReportDocument(input: RenderInput): input is ReportDocument {
|
|
@@ -82,9 +114,19 @@ function sectionMarkdown(section: ReportSection): string {
|
|
|
82
114
|
: "No data available.";
|
|
83
115
|
case "text":
|
|
84
116
|
return section.content;
|
|
117
|
+
case "diagram":
|
|
118
|
+
return section.mermaid === undefined
|
|
119
|
+
? fence("", section.text)
|
|
120
|
+
: fence("mermaid", section.mermaid);
|
|
121
|
+
case "diff":
|
|
122
|
+
return section.patch.trim() ? fence("diff", section.patch) : "No data available.";
|
|
85
123
|
}
|
|
86
124
|
}
|
|
87
125
|
|
|
126
|
+
function fence(language: string, content: string): string {
|
|
127
|
+
return `\`\`\`${language}\n${content}\n\`\`\``;
|
|
128
|
+
}
|
|
129
|
+
|
|
88
130
|
export function renderMarkdown(input: RenderInput): string {
|
|
89
131
|
const data = normalize(input);
|
|
90
132
|
const sections: string[] = [`# ${data.title}\n`];
|
|
@@ -153,6 +195,130 @@ function renderHTMLKeyValues(items: ReportKeyValue[]): string {
|
|
|
153
195
|
: emptyHTML("No metadata available.");
|
|
154
196
|
}
|
|
155
197
|
|
|
198
|
+
type DiffLineKind = "add" | "del" | "context" | "hunk";
|
|
199
|
+
|
|
200
|
+
interface DiffLine {
|
|
201
|
+
kind: DiffLineKind;
|
|
202
|
+
text: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface DiffFile {
|
|
206
|
+
path?: string;
|
|
207
|
+
lines: DiffLine[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const DIFF_METADATA_PREFIXES = [
|
|
211
|
+
"index ",
|
|
212
|
+
"old mode ",
|
|
213
|
+
"new mode ",
|
|
214
|
+
"new file mode ",
|
|
215
|
+
"deleted file mode ",
|
|
216
|
+
"similarity index ",
|
|
217
|
+
"dissimilarity index ",
|
|
218
|
+
"rename from ",
|
|
219
|
+
"rename to ",
|
|
220
|
+
"copy from ",
|
|
221
|
+
"copy to ",
|
|
222
|
+
"Binary files ",
|
|
223
|
+
"GIT binary patch",
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
function gitHeaderPath(line: string): string | undefined {
|
|
227
|
+
const rest = line.slice("diff --git ".length).trim();
|
|
228
|
+
const paired = /^a\/(.+?) b\/(.+)$/.exec(rest);
|
|
229
|
+
return paired ? paired[2] : rest || undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Strips the `a/` or `b/` prefix and the optional trailing timestamp git may append. */
|
|
233
|
+
function fileHeaderPath(value: string): string | undefined {
|
|
234
|
+
const path = value.split("\t")[0].trim();
|
|
235
|
+
if (path === "" || path === "/dev/null") return undefined;
|
|
236
|
+
return path.replace(/^[ab]\//, "");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function diffLineKind(line: string): DiffLineKind {
|
|
240
|
+
if (line.startsWith("+")) return "add";
|
|
241
|
+
if (line.startsWith("-")) return "del";
|
|
242
|
+
return "context";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** `---`/`+++` only head a file outside a hunk; inside one they are ordinary changed lines. */
|
|
246
|
+
function parseUnifiedDiff(patch: string): DiffFile[] {
|
|
247
|
+
const files: DiffFile[] = [];
|
|
248
|
+
let current: DiffFile | undefined;
|
|
249
|
+
let inHunk = false;
|
|
250
|
+
|
|
251
|
+
const open = (path?: string): DiffFile => {
|
|
252
|
+
const file: DiffFile = { path, lines: [] };
|
|
253
|
+
files.push(file);
|
|
254
|
+
return file;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
for (const line of patch.replace(/\n+$/, "").split("\n")) {
|
|
258
|
+
if (line.startsWith("diff --git ")) {
|
|
259
|
+
current = open(gitHeaderPath(line));
|
|
260
|
+
inHunk = false;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (!inHunk && (line.startsWith("--- ") || line.startsWith("+++ "))) {
|
|
264
|
+
const path = fileHeaderPath(line.slice(4));
|
|
265
|
+
if (current === undefined) current = open(path);
|
|
266
|
+
else current.path ??= path;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (line.startsWith("@@")) {
|
|
270
|
+
current ??= open();
|
|
271
|
+
current.lines.push({ kind: "hunk", text: line });
|
|
272
|
+
inHunk = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!inHunk && DIFF_METADATA_PREFIXES.some((prefix) => line.startsWith(prefix))) continue;
|
|
276
|
+
if (line === "" && current === undefined) continue;
|
|
277
|
+
current ??= open();
|
|
278
|
+
current.lines.push({ kind: diffLineKind(line), text: line });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return files.filter((file) => file.lines.length > 0);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function diffLanguage(path: string | undefined): string | undefined {
|
|
285
|
+
if (path === undefined) return undefined;
|
|
286
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
287
|
+
const dot = name.lastIndexOf(".");
|
|
288
|
+
return dot > 0 ? name.slice(dot + 1) : undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function renderHTMLDiffLine(line: DiffLine, language: string | undefined): string {
|
|
292
|
+
if (line.kind === "hunk") {
|
|
293
|
+
return `<div class="diff-line diff-hunk">${escapeHTML(line.text)}</div>`;
|
|
294
|
+
}
|
|
295
|
+
const glyph = /^[+\- ]/.test(line.text) ? line.text.slice(0, 1) : "";
|
|
296
|
+
const content = line.text.slice(glyph.length);
|
|
297
|
+
return `<div class="diff-line diff-${line.kind}"><span class="diff-glyph">${escapeHTML(glyph)}</span><span class="diff-code">${highlightInline(content, language)}</span></div>`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function renderHTMLDiff(section: ReportDiffSection): string {
|
|
301
|
+
const files = section.patch.trim() === "" ? [] : parseUnifiedDiff(section.patch);
|
|
302
|
+
if (files.length === 0) return emptyHTML("No diff available.");
|
|
303
|
+
|
|
304
|
+
return files
|
|
305
|
+
.map((file) => {
|
|
306
|
+
const language = diffLanguage(file.path);
|
|
307
|
+
const heading =
|
|
308
|
+
file.path === undefined ? "" : `<p class="diff-file">${escapeHTML(file.path)}</p>`;
|
|
309
|
+
const lines = file.lines.map((line) => renderHTMLDiffLine(line, language)).join("");
|
|
310
|
+
return `<div class="diff-file-group">${heading}<div class="report-diff">${lines}</div></div>`;
|
|
311
|
+
})
|
|
312
|
+
.join("");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function renderHTMLDiagram(section: ReportDiagramSection): string {
|
|
316
|
+
if (section.mermaid === undefined) {
|
|
317
|
+
return `<pre class="report-code report-diagram-text">${escapeHTML(section.text)}</pre>`;
|
|
318
|
+
}
|
|
319
|
+
return `<div class="report-diagram"><pre class="mermaid">${escapeHTML(section.mermaid)}</pre></div>`;
|
|
320
|
+
}
|
|
321
|
+
|
|
156
322
|
function renderHTMLSectionBody(section: ReportSection): string {
|
|
157
323
|
switch (section.type) {
|
|
158
324
|
case "metadata":
|
|
@@ -186,6 +352,10 @@ function renderHTMLSectionBody(section: ReportSection): string {
|
|
|
186
352
|
return section.content
|
|
187
353
|
? `<p class="report-text">${escapeHTML(section.content).replaceAll("\n", "<br>")}</p>`
|
|
188
354
|
: emptyHTML("No text available.");
|
|
355
|
+
case "diagram":
|
|
356
|
+
return renderHTMLDiagram(section);
|
|
357
|
+
case "diff":
|
|
358
|
+
return renderHTMLDiff(section);
|
|
189
359
|
}
|
|
190
360
|
}
|
|
191
361
|
|
|
@@ -210,6 +380,13 @@ export function renderHTML(input: RenderInput): string {
|
|
|
210
380
|
? ` data-working-repository="${escapeHTML(legacy.workingRepoPath)}" data-companion-repository="${escapeHTML(legacy.companionRepoPath)}"`
|
|
211
381
|
: "";
|
|
212
382
|
|
|
383
|
+
const visualStyles = carriesVisual(data.sections)
|
|
384
|
+
? `\n <style>${highlightStyleBlock(CODE_BLOCK_SELECTOR)}${VISUAL_SECTION_CSS}</style>`
|
|
385
|
+
: "";
|
|
386
|
+
const diagramRuntime = carriesMermaid(data.sections)
|
|
387
|
+
? `\n <script id="${MERMAID_RUNTIME_ID}">${readMermaidRuntime()}</script>\n <script>mermaid.initialize({ startOnLoad: true, securityLevel: "strict" });</script>`
|
|
388
|
+
: "";
|
|
389
|
+
|
|
213
390
|
return `<!doctype html>
|
|
214
391
|
<html lang="en">
|
|
215
392
|
<head>
|
|
@@ -241,7 +418,7 @@ export function renderHTML(input: RenderInput): string {
|
|
|
241
418
|
table { width: 100%; }
|
|
242
419
|
th, td { color: #000; }
|
|
243
420
|
}
|
|
244
|
-
</style
|
|
421
|
+
</style>${visualStyles}
|
|
245
422
|
</head>
|
|
246
423
|
<body>
|
|
247
424
|
<main class="report" data-report-version="${data.version}"${repositoryAttributes}>
|
|
@@ -253,11 +430,31 @@ export function renderHTML(input: RenderInput): string {
|
|
|
253
430
|
</header>
|
|
254
431
|
${renderHTMLSection(summary)}
|
|
255
432
|
${data.sections.map(renderHTMLSection).join("\n ")}
|
|
256
|
-
</main
|
|
433
|
+
</main>${diagramRuntime}
|
|
257
434
|
</body>
|
|
258
435
|
</html>`;
|
|
259
436
|
}
|
|
260
437
|
|
|
438
|
+
const VISUAL_SECTION_CSS = `
|
|
439
|
+
pre.report-code { background: #f7f9fb; border: 1px solid #e8edf1; border-radius: 10px; margin: 0; overflow-x: auto; padding: 14px 16px; }
|
|
440
|
+
.report-diagram { max-width: 100%; overflow-x: auto; }
|
|
441
|
+
.report-diagram svg { height: auto; max-width: 100%; }
|
|
442
|
+
.report-diagram .mermaid { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .82rem; margin: 0; white-space: pre; }
|
|
443
|
+
.diff-file-group + .diff-file-group { margin-top: 18px; }
|
|
444
|
+
.diff-file { color: #536273; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .8rem; font-weight: 700; margin: 0 0 8px; }
|
|
445
|
+
.report-diff { border: 1px solid #e8edf1; border-radius: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .82rem; overflow-x: auto; padding: 6px 0; }
|
|
446
|
+
.diff-line { -webkit-print-color-adjust: exact; min-width: max-content; padding: 0 12px; print-color-adjust: exact; white-space: pre; }
|
|
447
|
+
.diff-glyph { display: inline-block; user-select: none; width: 1ch; }
|
|
448
|
+
.diff-add { background: #e6ffec; }
|
|
449
|
+
.diff-del { background: #ffebe9; }
|
|
450
|
+
.diff-hunk { background: #eef2f6; color: #536273; }
|
|
451
|
+
@media print {
|
|
452
|
+
pre.report-code, .report-diff { border-color: #999; }
|
|
453
|
+
.report-diagram, .report-diff, pre.report-code { overflow: visible; }
|
|
454
|
+
.diff-line { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
455
|
+
.diff-hunk { color: #333; }
|
|
456
|
+
}`;
|
|
457
|
+
|
|
261
458
|
type ReportMetricsSection = Extract<ReportSection, { type: "metrics" }>;
|
|
262
459
|
|
|
263
460
|
export { escapeHTML };
|