@thinkingsage/kanon 0.8.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/CHANGELOG.md +410 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/bridge/mcp-server.cjs +14171 -0
- package/package.json +98 -0
- package/src/adapters/capabilities.ts +178 -0
- package/src/adapters/claude-code.ts +110 -0
- package/src/adapters/cline.ts +98 -0
- package/src/adapters/codex.ts +173 -0
- package/src/adapters/copilot.ts +106 -0
- package/src/adapters/cursor.ts +97 -0
- package/src/adapters/degradation.ts +95 -0
- package/src/adapters/index.ts +324 -0
- package/src/adapters/kiro-frontmatter.ts +139 -0
- package/src/adapters/kiro-inclusion.ts +86 -0
- package/src/adapters/kiro.ts +412 -0
- package/src/adapters/qdeveloper.ts +115 -0
- package/src/adapters/types.ts +81 -0
- package/src/adapters/windsurf.ts +96 -0
- package/src/admin.ts +283 -0
- package/src/asset-conventions.ts +118 -0
- package/src/attribution-backfill.ts +319 -0
- package/src/attribution-report.ts +95 -0
- package/src/attribution.ts +239 -0
- package/src/backends/github.ts +194 -0
- package/src/backends/http.ts +122 -0
- package/src/backends/index.ts +39 -0
- package/src/backends/local.ts +47 -0
- package/src/backends/s3.ts +157 -0
- package/src/backends/types.ts +59 -0
- package/src/base-cache.ts +270 -0
- package/src/browse-ui.ts +3754 -0
- package/src/browse.ts +1038 -0
- package/src/build.ts +1108 -0
- package/src/catalog.ts +204 -0
- package/src/cli-deprecated.ts +29 -0
- package/src/cli.ts +773 -0
- package/src/collection-admin.ts +287 -0
- package/src/collection-builder.ts +464 -0
- package/src/collections.ts +116 -0
- package/src/compatibility.ts +105 -0
- package/src/config.ts +743 -0
- package/src/eval/rubrics/kiro-progressive-steering.ts +841 -0
- package/src/eval.ts +1169 -0
- package/src/file-writer.ts +61 -0
- package/src/format-registry.ts +141 -0
- package/src/guild/auto-updater.ts +163 -0
- package/src/guild/backend-resolver.ts +49 -0
- package/src/guild/cli.ts +592 -0
- package/src/guild/collection-expander.ts +47 -0
- package/src/guild/global-cache.ts +247 -0
- package/src/guild/hook-generator.ts +100 -0
- package/src/guild/manifest.ts +154 -0
- package/src/guild/path-utils.ts +12 -0
- package/src/guild/sync.ts +622 -0
- package/src/guild/version-resolver.ts +42 -0
- package/src/help/metadata.ts +445 -0
- package/src/help/renderer.ts +265 -0
- package/src/help/typo-suggester.ts +25 -0
- package/src/hooks/expression.ts +493 -0
- package/src/hooks/pipeline.ts +141 -0
- package/src/import.ts +773 -0
- package/src/importers/claude-code.ts +134 -0
- package/src/importers/cline.ts +103 -0
- package/src/importers/codex.ts +140 -0
- package/src/importers/copilot.ts +103 -0
- package/src/importers/cursor.ts +105 -0
- package/src/importers/index.ts +390 -0
- package/src/importers/kiro.ts +110 -0
- package/src/importers/qdeveloper.ts +103 -0
- package/src/importers/types.ts +54 -0
- package/src/importers/windsurf.ts +104 -0
- package/src/install.ts +1005 -0
- package/src/manifest-admin.ts +306 -0
- package/src/mcp-bridge.ts +240 -0
- package/src/mutation/delta.ts +50 -0
- package/src/mutation/history.ts +66 -0
- package/src/mutation/operators.ts +524 -0
- package/src/mutation/runner.ts +332 -0
- package/src/new.ts +106 -0
- package/src/outcomes/collision.ts +127 -0
- package/src/outcomes/normalize.ts +208 -0
- package/src/outcomes/registry.ts +173 -0
- package/src/parser.ts +446 -0
- package/src/provenance-backfill-cli.ts +319 -0
- package/src/provenance-backfill.ts +520 -0
- package/src/publish.ts +354 -0
- package/src/reconcile-orchestrator.ts +502 -0
- package/src/reconcile-report-renderer.ts +176 -0
- package/src/resolve-body.ts +15 -0
- package/src/rosetta/builtins/compatibility-profiles.ts +297 -0
- package/src/rosetta/builtins/contracts.ts +1033 -0
- package/src/rosetta/builtins/pretty-printers/claude-code-native.ts +122 -0
- package/src/rosetta/builtins/pretty-printers/cline-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/codex-native.ts +127 -0
- package/src/rosetta/builtins/pretty-printers/copilot-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/cursor-native.ts +50 -0
- package/src/rosetta/builtins/pretty-printers/index.ts +81 -0
- package/src/rosetta/builtins/pretty-printers/kiro-native.ts +166 -0
- package/src/rosetta/builtins/pretty-printers/kiro-power.ts +108 -0
- package/src/rosetta/builtins/pretty-printers/kiro-skill.ts +88 -0
- package/src/rosetta/builtins/pretty-printers/qdeveloper-native.ts +51 -0
- package/src/rosetta/builtins/pretty-printers/superpowers.ts +97 -0
- package/src/rosetta/builtins/pretty-printers/windsurf-native.ts +50 -0
- package/src/rosetta/builtins/sources/claude-code-native.ts +348 -0
- package/src/rosetta/builtins/sources/cline-native.ts +176 -0
- package/src/rosetta/builtins/sources/codex-native.ts +343 -0
- package/src/rosetta/builtins/sources/copilot-native.ts +178 -0
- package/src/rosetta/builtins/sources/cursor-native.ts +176 -0
- package/src/rosetta/builtins/sources/index.ts +95 -0
- package/src/rosetta/builtins/sources/kiro-native.ts +462 -0
- package/src/rosetta/builtins/sources/kiro-power.ts +285 -0
- package/src/rosetta/builtins/sources/kiro-skill.ts +230 -0
- package/src/rosetta/builtins/sources/qdeveloper-native.ts +181 -0
- package/src/rosetta/builtins/sources/superpowers.ts +240 -0
- package/src/rosetta/builtins/sources/windsurf-native.ts +176 -0
- package/src/rosetta/builtins/targets/claude-code.ts +181 -0
- package/src/rosetta/builtins/targets/cline.ts +87 -0
- package/src/rosetta/builtins/targets/codex.ts +226 -0
- package/src/rosetta/builtins/targets/copilot.ts +103 -0
- package/src/rosetta/builtins/targets/cursor.ts +87 -0
- package/src/rosetta/builtins/targets/index.ts +60 -0
- package/src/rosetta/builtins/targets/kiro.ts +278 -0
- package/src/rosetta/builtins/targets/qdeveloper.ts +103 -0
- package/src/rosetta/builtins/targets/windsurf.ts +87 -0
- package/src/rosetta/canonical.ts +729 -0
- package/src/rosetta/compatibility.ts +432 -0
- package/src/rosetta/contracts.ts +329 -0
- package/src/rosetta/detector.ts +724 -0
- package/src/rosetta/diagnostics.ts +630 -0
- package/src/rosetta/engine-bootstrap.ts +103 -0
- package/src/rosetta/engine.ts +744 -0
- package/src/rosetta/index.ts +381 -0
- package/src/rosetta/inspection.ts +530 -0
- package/src/rosetta/plan.ts +448 -0
- package/src/rosetta/provenance-digest.ts +369 -0
- package/src/rosetta/reconcile.ts +812 -0
- package/src/rosetta/redaction.ts +467 -0
- package/src/rosetta/registry.ts +712 -0
- package/src/rosetta/renderers.ts +571 -0
- package/src/rosetta/request-guard.ts +335 -0
- package/src/rosetta/resolution.ts +419 -0
- package/src/rosetta/source-accounting.ts +233 -0
- package/src/rosetta/templates.ts +129 -0
- package/src/rosetta-cli.ts +717 -0
- package/src/rosetta-docs-generator.ts +793 -0
- package/src/rosetta-profiles-cli.ts +367 -0
- package/src/schemas.ts +1712 -0
- package/src/spec-coordination.ts +1141 -0
- package/src/temper.ts +747 -0
- package/src/template-bundle-loader.ts +312 -0
- package/src/template-engine.ts +53 -0
- package/src/translation-application-policy.ts +496 -0
- package/src/translation-orchestrator.ts +1013 -0
- package/src/translation-plan-applier.ts +473 -0
- package/src/tutorial.ts +305 -0
- package/src/validate.ts +1093 -0
- package/src/versioning.ts +553 -0
- package/src/wizard.ts +660 -0
- package/src/workspace.ts +237 -0
- package/templates/eval-contexts/claude-code.md.njk +6 -0
- package/templates/eval-contexts/cline.md.njk +6 -0
- package/templates/eval-contexts/copilot.md.njk +6 -0
- package/templates/eval-contexts/cursor.md.njk +6 -0
- package/templates/eval-contexts/kiro.md.njk +10 -0
- package/templates/eval-contexts/qdeveloper.md.njk +6 -0
- package/templates/eval-contexts/windsurf.md.njk +6 -0
- package/templates/harness-adapters/_base/attribution-footer.md.njk +17 -0
- package/templates/harness-adapters/_base/base.md.njk +16 -0
- package/templates/harness-adapters/claude-code/claude.md.njk +1 -0
- package/templates/harness-adapters/claude-code/mcp.json.njk +1 -0
- package/templates/harness-adapters/claude-code/settings.json.njk +1 -0
- package/templates/harness-adapters/claude-code/skill-library-index.md.njk +13 -0
- package/templates/harness-adapters/claude-code/skill.md.njk +19 -0
- package/templates/harness-adapters/cline/hook.sh.njk +4 -0
- package/templates/harness-adapters/cline/mcp.json.njk +1 -0
- package/templates/harness-adapters/cline/rule.md.njk +1 -0
- package/templates/harness-adapters/codex/agents-md.md.njk +6 -0
- package/templates/harness-adapters/codex/agents-pointer.md.njk +16 -0
- package/templates/harness-adapters/codex/skill.md.njk +27 -0
- package/templates/harness-adapters/copilot/agents.md.njk +1 -0
- package/templates/harness-adapters/copilot/instructions.md.njk +1 -0
- package/templates/harness-adapters/copilot/scoped.md.njk +6 -0
- package/templates/harness-adapters/cursor/mcp.json.njk +1 -0
- package/templates/harness-adapters/cursor/rule.md.njk +6 -0
- package/templates/harness-adapters/kiro/hook.json.njk +1 -0
- package/templates/harness-adapters/kiro/mcp.json.njk +1 -0
- package/templates/harness-adapters/kiro/power-steering.md.njk +3 -0
- package/templates/harness-adapters/kiro/power.md.njk +12 -0
- package/templates/harness-adapters/kiro/steering.md.njk +16 -0
- package/templates/harness-adapters/qdeveloper/agent.md.njk +1 -0
- package/templates/harness-adapters/qdeveloper/mcp.json.njk +1 -0
- package/templates/harness-adapters/qdeveloper/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/mcp.json.njk +1 -0
- package/templates/harness-adapters/windsurf/rule.md.njk +1 -0
- package/templates/harness-adapters/windsurf/workflow.md.njk +1 -0
- package/templates/knowledge/hooks.yaml.njk +4 -0
- package/templates/knowledge/knowledge.md.njk +53 -0
- package/templates/knowledge/mcp-servers.yaml.njk +2 -0
package/src/temper.ts
ADDED
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import { exists, readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import type { HarnessCapabilityName } from "./adapters/capabilities";
|
|
5
|
+
import {
|
|
6
|
+
getCapabilities,
|
|
7
|
+
getDegradation,
|
|
8
|
+
HARNESS_CAPABILITIES,
|
|
9
|
+
} from "./adapters/capabilities";
|
|
10
|
+
import { adapterRegistry } from "./adapters/index";
|
|
11
|
+
import type { AdapterContext } from "./adapters/types";
|
|
12
|
+
import { documentsAgentLoop } from "./asset-conventions";
|
|
13
|
+
import { generateCatalog, SOURCE_DIRS } from "./catalog";
|
|
14
|
+
import { isParseError, loadKnowledgeArtifact } from "./parser";
|
|
15
|
+
import type {
|
|
16
|
+
HarnessName,
|
|
17
|
+
KnowledgeArtifact,
|
|
18
|
+
TemperOutput,
|
|
19
|
+
TemperSection,
|
|
20
|
+
} from "./schemas";
|
|
21
|
+
import { isStdioServer, SUPPORTED_HARNESSES } from "./schemas";
|
|
22
|
+
import { createTemplateEnv } from "./template-engine";
|
|
23
|
+
|
|
24
|
+
export interface TemperOptions {
|
|
25
|
+
artifactName: string;
|
|
26
|
+
harness: HarnessName;
|
|
27
|
+
noColor?: boolean;
|
|
28
|
+
knowledgeDirs?: string[];
|
|
29
|
+
templatesDir?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Collect all artifact paths from one or more source directories.
|
|
34
|
+
* Mirrors the logic in build.ts for locating artifacts.
|
|
35
|
+
*/
|
|
36
|
+
async function collectArtifactPaths(sourceDirs: string[]): Promise<string[]> {
|
|
37
|
+
const paths: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (const sourceDir of sourceDirs) {
|
|
40
|
+
if (!(await exists(sourceDir))) continue;
|
|
41
|
+
|
|
42
|
+
const dirEntries = await readdir(sourceDir, { withFileTypes: true });
|
|
43
|
+
const subdirs = dirEntries
|
|
44
|
+
.filter((e) => e.isDirectory())
|
|
45
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
46
|
+
|
|
47
|
+
for (const subdir of subdirs) {
|
|
48
|
+
const subdirPath = join(sourceDir, subdir.name);
|
|
49
|
+
|
|
50
|
+
if (await exists(join(subdirPath, "knowledge.md"))) {
|
|
51
|
+
paths.push(subdirPath);
|
|
52
|
+
} else {
|
|
53
|
+
// Namespaced layout — recurse one level
|
|
54
|
+
const inner = await readdir(subdirPath, { withFileTypes: true });
|
|
55
|
+
const innerDirs = inner
|
|
56
|
+
.filter((e) => e.isDirectory())
|
|
57
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
|
+
|
|
59
|
+
for (const innerDir of innerDirs) {
|
|
60
|
+
const artifactPath = join(subdirPath, innerDir.name);
|
|
61
|
+
if (await exists(join(artifactPath, "knowledge.md"))) {
|
|
62
|
+
paths.push(artifactPath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return paths;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Find an artifact by name from the knowledge directories.
|
|
74
|
+
*/
|
|
75
|
+
async function findArtifact(
|
|
76
|
+
artifactName: string,
|
|
77
|
+
knowledgeDirs: string[],
|
|
78
|
+
): Promise<KnowledgeArtifact | null> {
|
|
79
|
+
const artifactPaths = await collectArtifactPaths(knowledgeDirs);
|
|
80
|
+
|
|
81
|
+
for (const artifactPath of artifactPaths) {
|
|
82
|
+
const result = await loadKnowledgeArtifact(artifactPath);
|
|
83
|
+
if (isParseError(result)) continue;
|
|
84
|
+
if (result.data.name === artifactName) {
|
|
85
|
+
return result.data;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Render a system prompt section using eval-contexts templates.
|
|
94
|
+
* Falls back to raw steering content if no template exists for the harness.
|
|
95
|
+
*/
|
|
96
|
+
function renderSystemPrompt(
|
|
97
|
+
harness: HarnessName,
|
|
98
|
+
steeringContent: string,
|
|
99
|
+
templatesDir: string,
|
|
100
|
+
): string {
|
|
101
|
+
const evalContextsDir = join(templatesDir, "../eval-contexts");
|
|
102
|
+
const templateEnv = createTemplateEnv(evalContextsDir);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const rendered = templateEnv.render(`${harness}.md.njk`, {
|
|
106
|
+
prompt: steeringContent,
|
|
107
|
+
user_query: "[user query placeholder]",
|
|
108
|
+
});
|
|
109
|
+
return rendered;
|
|
110
|
+
} catch {
|
|
111
|
+
// Template not found — return raw content
|
|
112
|
+
return steeringContent;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compile an artifact for a harness and produce a TemperOutput with all sections.
|
|
118
|
+
*
|
|
119
|
+
* Sections produced:
|
|
120
|
+
* - system-prompt: from eval-contexts template wrapping the compiled steering
|
|
121
|
+
* - steering: the main compiled markdown body
|
|
122
|
+
* - hooks: translated hooks (or degraded hooks info)
|
|
123
|
+
* - mcp-servers: MCP server definitions
|
|
124
|
+
* - degradation-report: when any capabilities were degraded
|
|
125
|
+
*/
|
|
126
|
+
export async function renderTemper(
|
|
127
|
+
options: TemperOptions,
|
|
128
|
+
): Promise<TemperOutput> {
|
|
129
|
+
const {
|
|
130
|
+
artifactName,
|
|
131
|
+
harness,
|
|
132
|
+
knowledgeDirs = [...SOURCE_DIRS],
|
|
133
|
+
templatesDir = "templates/harness-adapters",
|
|
134
|
+
} = options;
|
|
135
|
+
|
|
136
|
+
// 1. Load the artifact from knowledge dirs
|
|
137
|
+
const artifact = await findArtifact(artifactName, knowledgeDirs);
|
|
138
|
+
if (!artifact) {
|
|
139
|
+
// Return an error output with available artifacts listed
|
|
140
|
+
const catalog = await generateCatalog(knowledgeDirs);
|
|
141
|
+
const available = catalog.map((e) => e.name).join(", ");
|
|
142
|
+
return {
|
|
143
|
+
artifactName,
|
|
144
|
+
harnessName: harness,
|
|
145
|
+
sections: [
|
|
146
|
+
{
|
|
147
|
+
title: "Error",
|
|
148
|
+
content: `Artifact "${artifactName}" not found. Available artifacts: ${available}`,
|
|
149
|
+
type: "steering",
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
degradations: [],
|
|
153
|
+
fileCount: 0,
|
|
154
|
+
hooksTranslated: 0,
|
|
155
|
+
hooksDegraded: 0,
|
|
156
|
+
mcpServers: [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check if harness is in the artifact's harnesses list
|
|
161
|
+
if (!artifact.frontmatter.harnesses.includes(harness)) {
|
|
162
|
+
return {
|
|
163
|
+
artifactName,
|
|
164
|
+
harnessName: harness,
|
|
165
|
+
sections: [
|
|
166
|
+
{
|
|
167
|
+
title: "Error",
|
|
168
|
+
content: `Artifact "${artifactName}" does not target harness "${harness}". Supported harnesses: ${artifact.frontmatter.harnesses.join(", ")}`,
|
|
169
|
+
type: "steering",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
degradations: [],
|
|
173
|
+
fileCount: 0,
|
|
174
|
+
hooksTranslated: 0,
|
|
175
|
+
hooksDegraded: 0,
|
|
176
|
+
mcpServers: [],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 2. Compile it for the specified harness using the adapter
|
|
181
|
+
const adapter = adapterRegistry[harness];
|
|
182
|
+
const templateEnv = createTemplateEnv(templatesDir);
|
|
183
|
+
const capabilities = getCapabilities(harness);
|
|
184
|
+
const adapterContext: AdapterContext = {
|
|
185
|
+
capabilities,
|
|
186
|
+
strict: false,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const adapterResult = adapter(artifact, templateEnv, adapterContext);
|
|
190
|
+
|
|
191
|
+
// 3. Check capabilities and identify degradations
|
|
192
|
+
const degradations: string[] = [];
|
|
193
|
+
let hooksDegraded = 0;
|
|
194
|
+
let hooksTranslated = 0;
|
|
195
|
+
|
|
196
|
+
for (const cap of HARNESS_CAPABILITIES) {
|
|
197
|
+
const degradation = getDegradation(harness, cap as HarnessCapabilityName);
|
|
198
|
+
if (degradation) {
|
|
199
|
+
// Check if this capability is actually used by the artifact
|
|
200
|
+
const isUsed = isCapabilityUsed(artifact, cap as HarnessCapabilityName);
|
|
201
|
+
if (isUsed) {
|
|
202
|
+
degradations.push(`${cap}: ${degradation}`);
|
|
203
|
+
if (cap === "hooks") {
|
|
204
|
+
hooksDegraded = artifact.hooks.length;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Count hooks translated (fully supported)
|
|
211
|
+
if (capabilities.hooks.support === "full") {
|
|
212
|
+
hooksTranslated = artifact.hooks.length;
|
|
213
|
+
} else if (capabilities.hooks.support === "partial") {
|
|
214
|
+
// Partial: some translated, some degraded
|
|
215
|
+
hooksTranslated = Math.ceil(artifact.hooks.length / 2);
|
|
216
|
+
hooksDegraded = artifact.hooks.length - hooksTranslated;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 4. Build sections array
|
|
220
|
+
const sections: TemperSection[] = [];
|
|
221
|
+
|
|
222
|
+
// Get the main steering content from compiled files
|
|
223
|
+
const steeringContent = adapterResult.files
|
|
224
|
+
.filter((f) => f.relativePath.endsWith(".md"))
|
|
225
|
+
.map((f) => f.content)
|
|
226
|
+
.join("\n\n");
|
|
227
|
+
|
|
228
|
+
// System prompt section — use eval-contexts template
|
|
229
|
+
const systemPromptContent = renderSystemPrompt(
|
|
230
|
+
harness,
|
|
231
|
+
steeringContent,
|
|
232
|
+
templatesDir,
|
|
233
|
+
);
|
|
234
|
+
sections.push({
|
|
235
|
+
title: "System Prompt",
|
|
236
|
+
content: systemPromptContent,
|
|
237
|
+
type: "system-prompt",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Steering section — the raw compiled body
|
|
241
|
+
sections.push({
|
|
242
|
+
title: "Steering",
|
|
243
|
+
content: steeringContent || artifact.body,
|
|
244
|
+
type: "steering",
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Hooks section
|
|
248
|
+
if (artifact.hooks.length > 0) {
|
|
249
|
+
const hooksContent = artifact.hooks
|
|
250
|
+
.map((hook) => {
|
|
251
|
+
const action =
|
|
252
|
+
hook.action.type === "ask_agent"
|
|
253
|
+
? `Ask agent: ${hook.action.prompt}`
|
|
254
|
+
: `Run: ${hook.action.command}`;
|
|
255
|
+
const condition = hook.condition
|
|
256
|
+
? ` (${hook.condition.file_patterns?.join(", ") || hook.condition.tool_types?.join(", ") || ""})`
|
|
257
|
+
: "";
|
|
258
|
+
return `- ${hook.name}: on ${hook.event}${condition} → ${action}`;
|
|
259
|
+
})
|
|
260
|
+
.join("\n");
|
|
261
|
+
sections.push({
|
|
262
|
+
title: "Hooks",
|
|
263
|
+
content: hooksContent,
|
|
264
|
+
type: "hooks",
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// MCP servers section
|
|
269
|
+
if (artifact.mcpServers.length > 0) {
|
|
270
|
+
const mcpContent = artifact.mcpServers
|
|
271
|
+
.map((server) => {
|
|
272
|
+
if (isStdioServer(server)) {
|
|
273
|
+
const args =
|
|
274
|
+
server.args.length > 0 ? ` ${server.args.join(" ")}` : "";
|
|
275
|
+
const envStr =
|
|
276
|
+
Object.keys(server.env).length > 0
|
|
277
|
+
? `\n env: ${JSON.stringify(server.env)}`
|
|
278
|
+
: "";
|
|
279
|
+
return `- ${server.name}: ${server.command}${args}${envStr}`;
|
|
280
|
+
}
|
|
281
|
+
return `- ${server.name}: ${server.url}`;
|
|
282
|
+
})
|
|
283
|
+
.join("\n");
|
|
284
|
+
sections.push({
|
|
285
|
+
title: "MCP Servers",
|
|
286
|
+
content: mcpContent,
|
|
287
|
+
type: "mcp-servers",
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Degradation report section (only when applicable)
|
|
292
|
+
if (degradations.length > 0) {
|
|
293
|
+
const reportContent = degradations.map((d) => `- ${d}`).join("\n");
|
|
294
|
+
sections.push({
|
|
295
|
+
title: "Degradation Report",
|
|
296
|
+
content: reportContent,
|
|
297
|
+
type: "degradation-report",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 5. Return the TemperOutput
|
|
302
|
+
return {
|
|
303
|
+
artifactName,
|
|
304
|
+
harnessName: harness,
|
|
305
|
+
sections,
|
|
306
|
+
degradations,
|
|
307
|
+
fileCount: adapterResult.files.length,
|
|
308
|
+
hooksTranslated,
|
|
309
|
+
hooksDegraded,
|
|
310
|
+
mcpServers: artifact.mcpServers.map((s) => s.name),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check if a capability is actually used by the artifact.
|
|
316
|
+
*/
|
|
317
|
+
function isCapabilityUsed(
|
|
318
|
+
artifact: KnowledgeArtifact,
|
|
319
|
+
capability: HarnessCapabilityName,
|
|
320
|
+
): boolean {
|
|
321
|
+
switch (capability) {
|
|
322
|
+
case "hooks":
|
|
323
|
+
return artifact.hooks.length > 0;
|
|
324
|
+
case "mcp":
|
|
325
|
+
return artifact.mcpServers.length > 0;
|
|
326
|
+
case "workflows":
|
|
327
|
+
return artifact.workflows.length > 0;
|
|
328
|
+
case "path_scoping":
|
|
329
|
+
return (
|
|
330
|
+
artifact.frontmatter.inclusion === "fileMatch" &&
|
|
331
|
+
(artifact.frontmatter.file_patterns?.length ?? 0) > 0
|
|
332
|
+
);
|
|
333
|
+
case "toggleable_rules":
|
|
334
|
+
return artifact.frontmatter.inclusion === "manual";
|
|
335
|
+
case "agents":
|
|
336
|
+
// Type-aware, not type-literal: an artifact whose body documents
|
|
337
|
+
// agent-loop behavior (goal/inputs/outputs/loop headings) gets
|
|
338
|
+
// degradation reporting even when its declared `type` is something
|
|
339
|
+
// else (e.g. "power") — see ADR-0050.
|
|
340
|
+
return (
|
|
341
|
+
artifact.frontmatter.type === "agent" ||
|
|
342
|
+
documentsAgentLoop(artifact.body)
|
|
343
|
+
);
|
|
344
|
+
case "file_match_inclusion":
|
|
345
|
+
return (artifact.frontmatter.file_patterns?.length ?? 0) > 0;
|
|
346
|
+
case "system_prompt_merging":
|
|
347
|
+
// Always relevant — steering content is always merged into system prompt
|
|
348
|
+
return true;
|
|
349
|
+
default:
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// --- Task 10.2: Terminal Output Formatting ---
|
|
355
|
+
|
|
356
|
+
export interface ComparisonResult {
|
|
357
|
+
artifactName: string;
|
|
358
|
+
harnesses: Array<{
|
|
359
|
+
harnessName: string;
|
|
360
|
+
fileCount: number;
|
|
361
|
+
hooksTranslated: number;
|
|
362
|
+
hooksDegraded: number;
|
|
363
|
+
mcpServers: string[];
|
|
364
|
+
degradations: string[];
|
|
365
|
+
}>;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Format TemperOutput as terminal text with chalk syntax highlighting.
|
|
370
|
+
* When noColor is true, strips all ANSI codes for deterministic output.
|
|
371
|
+
*/
|
|
372
|
+
export function formatTerminalOutput(
|
|
373
|
+
output: TemperOutput,
|
|
374
|
+
noColor?: boolean,
|
|
375
|
+
): string {
|
|
376
|
+
const lines: string[] = [];
|
|
377
|
+
|
|
378
|
+
if (noColor) {
|
|
379
|
+
// Deterministic plain-text output
|
|
380
|
+
lines.push(
|
|
381
|
+
`=== Temper: ${output.artifactName} @ ${output.harnessName} ===`,
|
|
382
|
+
);
|
|
383
|
+
lines.push("");
|
|
384
|
+
lines.push(
|
|
385
|
+
`Files: ${output.fileCount} | Hooks translated: ${output.hooksTranslated} | Hooks degraded: ${output.hooksDegraded} | MCP servers: ${output.mcpServers.length}`,
|
|
386
|
+
);
|
|
387
|
+
lines.push("");
|
|
388
|
+
|
|
389
|
+
for (const section of output.sections) {
|
|
390
|
+
lines.push(`--- ${section.title} (${section.type}) ---`);
|
|
391
|
+
lines.push(section.content);
|
|
392
|
+
lines.push("");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return lines.join("\n");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Colorized terminal output
|
|
399
|
+
lines.push(
|
|
400
|
+
chalk.bold.cyan(
|
|
401
|
+
`=== Temper: ${output.artifactName} @ ${output.harnessName} ===`,
|
|
402
|
+
),
|
|
403
|
+
);
|
|
404
|
+
lines.push("");
|
|
405
|
+
|
|
406
|
+
const stats = [
|
|
407
|
+
`${chalk.dim("Files:")} ${chalk.white(String(output.fileCount))}`,
|
|
408
|
+
`${chalk.dim("Hooks translated:")} ${chalk.green(String(output.hooksTranslated))}`,
|
|
409
|
+
`${chalk.dim("Hooks degraded:")} ${output.hooksDegraded > 0 ? chalk.yellow(String(output.hooksDegraded)) : chalk.white("0")}`,
|
|
410
|
+
`${chalk.dim("MCP servers:")} ${chalk.white(String(output.mcpServers.length))}`,
|
|
411
|
+
];
|
|
412
|
+
lines.push(stats.join(" | "));
|
|
413
|
+
lines.push("");
|
|
414
|
+
|
|
415
|
+
for (const section of output.sections) {
|
|
416
|
+
const typeColor = getSectionColor(section.type);
|
|
417
|
+
lines.push(
|
|
418
|
+
chalk.bold(typeColor(`--- ${section.title} (${section.type}) ---`)),
|
|
419
|
+
);
|
|
420
|
+
lines.push(section.content);
|
|
421
|
+
lines.push("");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return lines.join("\n");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function getSectionColor(type: string): (text: string) => string {
|
|
428
|
+
switch (type) {
|
|
429
|
+
case "system-prompt":
|
|
430
|
+
return chalk.blue;
|
|
431
|
+
case "steering":
|
|
432
|
+
return chalk.green;
|
|
433
|
+
case "hooks":
|
|
434
|
+
return chalk.magenta;
|
|
435
|
+
case "mcp-servers":
|
|
436
|
+
return chalk.cyan;
|
|
437
|
+
case "degradation-report":
|
|
438
|
+
return chalk.yellow;
|
|
439
|
+
default:
|
|
440
|
+
return chalk.white;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// --- Task 10.3: JSON Output Mode ---
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Serialize TemperOutput as valid JSON conforming to TemperOutputSchema.
|
|
448
|
+
*/
|
|
449
|
+
export function formatJsonOutput(output: TemperOutput): string {
|
|
450
|
+
return JSON.stringify(output, null, 2);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// --- Task 10.4: Side-by-Side Comparison ---
|
|
454
|
+
|
|
455
|
+
export interface RenderComparisonOptions {
|
|
456
|
+
artifactName: string;
|
|
457
|
+
harnesses: HarnessName[];
|
|
458
|
+
knowledgeDirs?: string[];
|
|
459
|
+
templatesDir?: string;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Compile artifact for multiple harnesses and produce a comparison summary.
|
|
464
|
+
*/
|
|
465
|
+
export async function renderComparison(
|
|
466
|
+
options: RenderComparisonOptions,
|
|
467
|
+
): Promise<ComparisonResult> {
|
|
468
|
+
const {
|
|
469
|
+
artifactName,
|
|
470
|
+
harnesses,
|
|
471
|
+
knowledgeDirs = [...SOURCE_DIRS],
|
|
472
|
+
templatesDir = "templates/harness-adapters",
|
|
473
|
+
} = options;
|
|
474
|
+
|
|
475
|
+
const harnessResults: ComparisonResult["harnesses"] = [];
|
|
476
|
+
|
|
477
|
+
for (const harness of harnesses) {
|
|
478
|
+
const output = await renderTemper({
|
|
479
|
+
artifactName,
|
|
480
|
+
harness,
|
|
481
|
+
knowledgeDirs,
|
|
482
|
+
templatesDir,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
harnessResults.push({
|
|
486
|
+
harnessName: harness,
|
|
487
|
+
fileCount: output.fileCount,
|
|
488
|
+
hooksTranslated: output.hooksTranslated,
|
|
489
|
+
hooksDegraded: output.hooksDegraded,
|
|
490
|
+
mcpServers: output.mcpServers,
|
|
491
|
+
degradations: output.degradations,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
artifactName,
|
|
497
|
+
harnesses: harnessResults,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Format a ComparisonResult as a terminal-friendly table string.
|
|
503
|
+
*/
|
|
504
|
+
export function formatComparisonOutput(
|
|
505
|
+
result: ComparisonResult,
|
|
506
|
+
noColor?: boolean,
|
|
507
|
+
): string {
|
|
508
|
+
const lines: string[] = [];
|
|
509
|
+
|
|
510
|
+
if (noColor) {
|
|
511
|
+
lines.push(`=== Comparison: ${result.artifactName} ===`);
|
|
512
|
+
lines.push("");
|
|
513
|
+
|
|
514
|
+
// Header
|
|
515
|
+
const headers = [
|
|
516
|
+
"Harness",
|
|
517
|
+
"Files",
|
|
518
|
+
"Hooks OK",
|
|
519
|
+
"Hooks Degraded",
|
|
520
|
+
"MCP Servers",
|
|
521
|
+
"Degradations",
|
|
522
|
+
];
|
|
523
|
+
lines.push(headers.join(" | "));
|
|
524
|
+
lines.push(headers.map((h) => "-".repeat(h.length)).join("-+-"));
|
|
525
|
+
|
|
526
|
+
for (const h of result.harnesses) {
|
|
527
|
+
const row = [
|
|
528
|
+
h.harnessName.padEnd(headers[0].length),
|
|
529
|
+
String(h.fileCount).padEnd(headers[1].length),
|
|
530
|
+
String(h.hooksTranslated).padEnd(headers[2].length),
|
|
531
|
+
String(h.hooksDegraded).padEnd(headers[3].length),
|
|
532
|
+
String(h.mcpServers.length).padEnd(headers[4].length),
|
|
533
|
+
h.degradations.length > 0 ? h.degradations.join("; ") : "none",
|
|
534
|
+
];
|
|
535
|
+
lines.push(row.join(" | "));
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
lines.push(chalk.bold.cyan(`=== Comparison: ${result.artifactName} ===`));
|
|
539
|
+
lines.push("");
|
|
540
|
+
|
|
541
|
+
for (const h of result.harnesses) {
|
|
542
|
+
lines.push(chalk.bold.white(` ${h.harnessName}`));
|
|
543
|
+
lines.push(` ${chalk.dim("Files:")} ${h.fileCount}`);
|
|
544
|
+
lines.push(
|
|
545
|
+
` ${chalk.dim("Hooks translated:")} ${chalk.green(String(h.hooksTranslated))}`,
|
|
546
|
+
);
|
|
547
|
+
lines.push(
|
|
548
|
+
` ${chalk.dim("Hooks degraded:")} ${h.hooksDegraded > 0 ? chalk.yellow(String(h.hooksDegraded)) : "0"}`,
|
|
549
|
+
);
|
|
550
|
+
lines.push(
|
|
551
|
+
` ${chalk.dim("MCP servers:")} ${h.mcpServers.join(", ") || "none"}`,
|
|
552
|
+
);
|
|
553
|
+
if (h.degradations.length > 0) {
|
|
554
|
+
lines.push(
|
|
555
|
+
` ${chalk.dim("Degradations:")} ${chalk.yellow(h.degradations.join(", "))}`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
lines.push("");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return lines.join("\n");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// --- Task 10.5: Web Preview Mode ---
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Generate a self-contained HTML page for the temper web preview.
|
|
569
|
+
* Uses inline CSS/JS, syntax highlighting, collapsible sections, and harness selector.
|
|
570
|
+
*/
|
|
571
|
+
export function generateTemperHtml(output: TemperOutput): string {
|
|
572
|
+
const allHarnesses = [...SUPPORTED_HARNESSES];
|
|
573
|
+
const _sectionsJson = JSON.stringify(output.sections);
|
|
574
|
+
const _outputJson = JSON.stringify(output);
|
|
575
|
+
|
|
576
|
+
return `<!DOCTYPE html>
|
|
577
|
+
<html lang="en">
|
|
578
|
+
<head>
|
|
579
|
+
<meta charset="UTF-8">
|
|
580
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
581
|
+
<title>Temper Preview: ${escapeHtml(output.artifactName)} @ ${escapeHtml(output.harnessName)}</title>
|
|
582
|
+
<style>
|
|
583
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
584
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #e0e0e0; padding: 2rem; line-height: 1.6; }
|
|
585
|
+
.container { max-width: 960px; margin: 0 auto; }
|
|
586
|
+
header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid #333; }
|
|
587
|
+
h1 { font-size: 1.5rem; color: #64ffda; }
|
|
588
|
+
.stats { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
|
|
589
|
+
.stat { background: #16213e; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.875rem; }
|
|
590
|
+
.stat-label { color: #888; }
|
|
591
|
+
.stat-value { color: #fff; font-weight: 600; }
|
|
592
|
+
.harness-selector { background: #16213e; border: 1px solid #333; color: #e0e0e0; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.875rem; cursor: pointer; }
|
|
593
|
+
.section { margin-bottom: 1.5rem; border: 1px solid #333; border-radius: 8px; overflow: hidden; }
|
|
594
|
+
.section-header { background: #16213e; padding: 0.75rem 1rem; cursor: pointer; display: flex; align-items: center; justify-content: space-between; user-select: none; }
|
|
595
|
+
.section-header:hover { background: #1a2744; }
|
|
596
|
+
.section-title { font-weight: 600; font-size: 0.95rem; }
|
|
597
|
+
.section-type { font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px; }
|
|
598
|
+
.type-system-prompt { background: #1e3a5f; color: #64b5f6; }
|
|
599
|
+
.type-steering { background: #1b5e20; color: #81c784; }
|
|
600
|
+
.type-hooks { background: #4a148c; color: #ce93d8; }
|
|
601
|
+
.type-mcp-servers { background: #006064; color: #4dd0e1; }
|
|
602
|
+
.type-degradation-report { background: #e65100; color: #ffb74d; }
|
|
603
|
+
.section-content { padding: 1rem; background: #0f0f23; display: none; }
|
|
604
|
+
.section-content.open { display: block; }
|
|
605
|
+
.section-content pre { white-space: pre-wrap; word-wrap: break-word; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.85rem; line-height: 1.5; }
|
|
606
|
+
.chevron { transition: transform 0.2s; }
|
|
607
|
+
.chevron.open { transform: rotate(90deg); }
|
|
608
|
+
</style>
|
|
609
|
+
</head>
|
|
610
|
+
<body>
|
|
611
|
+
<div class="container">
|
|
612
|
+
<header>
|
|
613
|
+
<h1>Temper: ${escapeHtml(output.artifactName)}</h1>
|
|
614
|
+
<select class="harness-selector" id="harnessSelect">
|
|
615
|
+
${allHarnesses.map((h) => `<option value="${h}"${h === output.harnessName ? " selected" : ""}>${h}</option>`).join("\n ")}
|
|
616
|
+
</select>
|
|
617
|
+
</header>
|
|
618
|
+
|
|
619
|
+
<div class="stats">
|
|
620
|
+
<div class="stat"><span class="stat-label">Harness:</span> <span class="stat-value">${escapeHtml(output.harnessName)}</span></div>
|
|
621
|
+
<div class="stat"><span class="stat-label">Files:</span> <span class="stat-value">${output.fileCount}</span></div>
|
|
622
|
+
<div class="stat"><span class="stat-label">Hooks translated:</span> <span class="stat-value">${output.hooksTranslated}</span></div>
|
|
623
|
+
<div class="stat"><span class="stat-label">Hooks degraded:</span> <span class="stat-value">${output.hooksDegraded}</span></div>
|
|
624
|
+
<div class="stat"><span class="stat-label">MCP servers:</span> <span class="stat-value">${output.mcpServers.length}</span></div>
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
<div id="sections">
|
|
628
|
+
${output.sections
|
|
629
|
+
.map(
|
|
630
|
+
(s, i) => `<div class="section">
|
|
631
|
+
<div class="section-header" onclick="toggleSection(${i})">
|
|
632
|
+
<span class="section-title"><span class="chevron" id="chevron-${i}">▶</span> ${escapeHtml(s.title)}</span>
|
|
633
|
+
<span class="section-type type-${s.type}">${s.type}</span>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="section-content" id="section-${i}">
|
|
636
|
+
<pre>${escapeHtml(s.content)}</pre>
|
|
637
|
+
</div>
|
|
638
|
+
</div>`,
|
|
639
|
+
)
|
|
640
|
+
.join("\n")}
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
|
|
644
|
+
<script>
|
|
645
|
+
function toggleSection(idx) {
|
|
646
|
+
const el = document.getElementById('section-' + idx);
|
|
647
|
+
const chevron = document.getElementById('chevron-' + idx);
|
|
648
|
+
el.classList.toggle('open');
|
|
649
|
+
chevron.classList.toggle('open');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Expand first section by default
|
|
653
|
+
if (document.getElementById('section-0')) {
|
|
654
|
+
document.getElementById('section-0').classList.add('open');
|
|
655
|
+
document.getElementById('chevron-0').classList.add('open');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Harness selector — reload with new harness
|
|
659
|
+
document.getElementById('harnessSelect').addEventListener('change', function() {
|
|
660
|
+
const harness = this.value;
|
|
661
|
+
window.location.href = '/?harness=' + encodeURIComponent(harness);
|
|
662
|
+
});
|
|
663
|
+
</script>
|
|
664
|
+
</body>
|
|
665
|
+
</html>`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function escapeHtml(str: string): string {
|
|
669
|
+
return str
|
|
670
|
+
.replace(/&/g, "&")
|
|
671
|
+
.replace(/</g, "<")
|
|
672
|
+
.replace(/>/g, ">")
|
|
673
|
+
.replace(/"/g, """)
|
|
674
|
+
.replace(/'/g, "'");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Start a local HTTP server serving the temper HTML preview.
|
|
679
|
+
* Opens the browser and prints the URL to stderr.
|
|
680
|
+
*/
|
|
681
|
+
export async function startTemperServer(
|
|
682
|
+
output: TemperOutput,
|
|
683
|
+
port = 4400,
|
|
684
|
+
): Promise<void> {
|
|
685
|
+
const temperOptions: TemperOptions = {
|
|
686
|
+
artifactName: output.artifactName,
|
|
687
|
+
harness: output.harnessName as HarnessName,
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
const server = Bun.serve({
|
|
691
|
+
hostname: "localhost",
|
|
692
|
+
port,
|
|
693
|
+
async fetch(req) {
|
|
694
|
+
const url = new URL(req.url);
|
|
695
|
+
const harness = url.searchParams.get("harness") as HarnessName | null;
|
|
696
|
+
|
|
697
|
+
let currentOutput = output;
|
|
698
|
+
if (
|
|
699
|
+
harness &&
|
|
700
|
+
harness !== output.harnessName &&
|
|
701
|
+
SUPPORTED_HARNESSES.includes(harness as HarnessName)
|
|
702
|
+
) {
|
|
703
|
+
currentOutput = await renderTemper({
|
|
704
|
+
...temperOptions,
|
|
705
|
+
harness: harness as HarnessName,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const html = generateTemperHtml(currentOutput);
|
|
710
|
+
return new Response(html, {
|
|
711
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
712
|
+
});
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
const serverUrl = `http://localhost:${port}`;
|
|
717
|
+
console.error(
|
|
718
|
+
chalk.green(`Temper preview running at ${chalk.bold(serverUrl)}`),
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
// Attempt to open browser (best-effort)
|
|
722
|
+
try {
|
|
723
|
+
const platform = process.platform;
|
|
724
|
+
const cmd =
|
|
725
|
+
platform === "darwin"
|
|
726
|
+
? "open"
|
|
727
|
+
: platform === "win32"
|
|
728
|
+
? "start"
|
|
729
|
+
: "xdg-open";
|
|
730
|
+
Bun.spawn([cmd, serverUrl], { stdout: "ignore", stderr: "ignore" });
|
|
731
|
+
} catch {
|
|
732
|
+
// Silently ignore
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Keep server running until SIGINT
|
|
736
|
+
process.on("SIGINT", () => {
|
|
737
|
+
server.stop();
|
|
738
|
+
console.error(chalk.yellow("\nTemper server shut down."));
|
|
739
|
+
process.exit(0);
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// --- Task 10.6: Error Handling ---
|
|
744
|
+
// Error handling is already implemented in renderTemper() above:
|
|
745
|
+
// - Returns error listing available artifacts when artifact not found
|
|
746
|
+
// - Returns error when harness not in artifact's harnesses list
|
|
747
|
+
// Both error messages include actionable suggestions.
|