open-multi-agent-kit 0.78.6 → 0.78.7
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/AGENTS.md +1 -1
- package/CHANGELOG.md +26 -0
- package/MATURITY.md +2 -2
- package/README.md +19 -4
- package/dist/brand/night-city.theme.json +80 -0
- package/dist/brand/palette.d.ts +73 -177
- package/dist/brand/palette.js +115 -39
- package/dist/brand/rust-forge.theme.json +216 -0
- package/dist/brand/theme-compiled.d.ts +61 -0
- package/dist/brand/theme-compiled.js +241 -0
- package/dist/brand/theme.js +50 -37
- package/dist/cli/release-promotion-gate.d.ts +8 -1
- package/dist/cli/release-promotion-gate.js +20 -9
- package/dist/cli/root.d.ts +1 -0
- package/dist/cli/root.js +2 -1
- package/dist/cli/theme/index.d.ts +11 -2
- package/dist/cli/theme/index.js +6 -1
- package/dist/cli/theme/oklab-quantize.d.ts +29 -0
- package/dist/cli/theme/oklab-quantize.js +105 -0
- package/dist/cli/theme/render-table.d.ts +59 -0
- package/dist/cli/theme/render-table.js +112 -0
- package/dist/cli/theme/status-frame.d.ts +10 -0
- package/dist/cli/theme/status-frame.js +22 -0
- package/dist/cli/theme/terminal-capability.d.ts +6 -1
- package/dist/cli/theme/terminal-capability.js +20 -4
- package/dist/cli/theme/theme-doc.d.ts +22 -0
- package/dist/cli/theme/theme-doc.js +97 -0
- package/dist/cli/theme/theme-registry.js +39 -18
- package/dist/cli/theme/tier-explain.d.ts +21 -0
- package/dist/cli/theme/tier-explain.js +60 -0
- package/dist/cli/ui/green-rain-renderer.js +3 -2
- package/dist/cli/ui/neon-grid-renderer.js +4 -3
- package/dist/cli/ui/rust-forge-renderer.js +17 -3
- package/dist/cli/ui/system24-renderer.d.ts +18 -1
- package/dist/cli/ui/system24-renderer.js +39 -16
- package/dist/cli/v2/chat-repl.js +3 -1
- package/dist/cli/v2/cli-v2-skeleton.d.ts +6 -0
- package/dist/cli/v2/cli-v2-skeleton.js +121 -9
- package/dist/cli/v2/interactive-prompt.d.ts +1 -0
- package/dist/cli/v2/interactive-prompt.js +6 -2
- package/dist/commands/chat/core.js +2 -2
- package/dist/commands/chat/runtime.js +2 -0
- package/dist/commands/chat/utils.js +12 -4
- package/dist/commands/cockpit/render.js +102 -64
- package/dist/commands/cockpit/utils.d.ts +2 -0
- package/dist/commands/design.js +18 -16
- package/dist/commands/doctor/checks.d.ts +0 -1
- package/dist/commands/doctor/checks.js +1 -36
- package/dist/commands/doctor/report.js +0 -11
- package/dist/commands/init/content.d.ts +1 -1
- package/dist/commands/init/content.js +3 -11
- package/dist/commands/init.js +2 -2
- package/dist/commands/mcp/config.d.ts +32 -0
- package/dist/commands/mcp/config.js +264 -0
- package/dist/commands/mcp/doctor-fix.d.ts +12 -0
- package/dist/commands/mcp/doctor-fix.js +261 -0
- package/dist/commands/mcp/doctor.d.ts +55 -0
- package/dist/commands/mcp/doctor.js +401 -0
- package/dist/commands/mcp/list.d.ts +1 -0
- package/dist/commands/mcp/list.js +56 -0
- package/dist/commands/mcp/shared.d.ts +47 -0
- package/dist/commands/mcp/shared.js +192 -0
- package/dist/commands/mcp/test.d.ts +5 -0
- package/dist/commands/mcp/test.js +457 -0
- package/dist/commands/mcp.d.ts +8 -104
- package/dist/commands/mcp.js +5 -1603
- package/dist/contracts/proof.d.ts +1 -1
- package/dist/goal/control-loop.d.ts +15 -0
- package/dist/goal/control-loop.js +39 -5
- package/dist/hud/render.d.ts +2 -0
- package/dist/hud/render.js +7 -1
- package/dist/mcp/secret-scanner.d.ts +6 -0
- package/dist/mcp/secret-scanner.js +44 -11
- package/dist/memory/graph-delta-log.d.ts +137 -0
- package/dist/memory/graph-delta-log.js +650 -0
- package/dist/memory/graph-viewer.js +13 -32
- package/dist/memory/local-graph-memory-store.d.ts +36 -3
- package/dist/memory/local-graph-memory-store.js +255 -10
- package/dist/orchestration/log-streamer.js +13 -8
- package/dist/orchestration/routing.d.ts +4 -0
- package/dist/orchestration/routing.js +28 -3
- package/dist/orchestration/state-persister.js +8 -1
- package/dist/providers/anthropic-messages-runner.d.ts +17 -0
- package/dist/providers/anthropic-messages-runner.js +197 -0
- package/dist/providers/model-registry.d.ts +1 -1
- package/dist/providers/model-registry.js +37 -3
- package/dist/providers/provider-runtime.js +27 -11
- package/dist/providers/thinking-levels.js +4 -1
- package/dist/providers/types.d.ts +3 -3
- package/dist/runtime/contracts/weakness-remediation.d.ts +9 -1
- package/dist/runtime/contracts/weakness-remediation.js +9 -1
- package/dist/runtime/router-v2-scoring.d.ts +8 -3
- package/dist/runtime/router-v2-scoring.js +27 -22
- package/dist/runtime/runtime-router.d.ts +5 -0
- package/dist/runtime/runtime-router.js +40 -8
- package/dist/runtime/sandbox-profile.d.ts +29 -0
- package/dist/runtime/sandbox-profile.js +101 -0
- package/dist/runtime/slash-commands.js +300 -2
- package/dist/runtime/tool-dispatch-contracts.d.ts +15 -0
- package/dist/runtime/tool-dispatch-contracts.js +12 -0
- package/dist/runtime/weights-config.d.ts +90 -0
- package/dist/runtime/weights-config.js +249 -0
- package/dist/schema/envelope.schema.d.ts +18 -18
- package/dist/schema/evidence.schema.d.ts +2 -2
- package/dist/schema/proof-bundle.schema.d.ts +6 -6
- package/dist/schema/proof-bundle.schema.js +0 -1
- package/dist/schema/provider.schema.d.ts +2 -2
- package/dist/schema/run-manifest.schema.d.ts +16 -16
- package/dist/theme/ansi.js +9 -2
- package/dist/theme/extended-palette.d.ts +85 -0
- package/dist/theme/extended-palette.js +108 -0
- package/dist/theme/layout.js +29 -8
- package/dist/theme/metrics.js +5 -5
- package/dist/theme/parallel.js +25 -15
- package/dist/ui/omk-sigil.js +5 -14
- package/dist/ui/omk-working-sweep.js +5 -15
- package/dist/util/chat-agent-mode.d.ts +11 -0
- package/dist/util/chat-agent-mode.js +41 -8
- package/dist/util/chat-cockpit.d.ts +9 -0
- package/dist/util/chat-cockpit.js +77 -20
- package/dist/util/fs/core.d.ts +5 -0
- package/dist/util/fs/core.js +35 -0
- package/dist/util/fs/internal.d.ts +14 -0
- package/dist/util/fs/internal.js +169 -0
- package/dist/util/fs/kimi-sync.d.ts +41 -0
- package/dist/util/fs/kimi-sync.js +495 -0
- package/dist/util/fs/logo.d.ts +2 -0
- package/dist/util/fs/logo.js +78 -0
- package/dist/util/fs/manifest.d.ts +6 -0
- package/dist/util/fs/manifest.js +40 -0
- package/dist/util/fs/mcp-diagnose.d.ts +15 -0
- package/dist/util/fs/mcp-diagnose.js +204 -0
- package/dist/util/fs/mcp-runtime-config.d.ts +23 -0
- package/dist/util/fs/mcp-runtime-config.js +412 -0
- package/dist/util/fs/paths.d.ts +13 -0
- package/dist/util/fs/paths.js +82 -0
- package/dist/util/fs/preflight.d.ts +27 -0
- package/dist/util/fs/preflight.js +271 -0
- package/dist/util/fs.d.ts +8 -128
- package/dist/util/fs.js +8 -1757
- package/dist/util/terminal-layout.js +5 -1
- package/dist/util/version.js +2 -1
- package/docs/2026-06-11/critical-issues.md +20 -0
- package/docs/2026-06-11/improvements.md +14 -0
- package/docs/2026-06-11/init-checklist.md +25 -0
- package/docs/2026-06-11/plan.md +20 -0
- package/docs/ARCHITECTURE_ANALYSIS_CODEGRAPH.md +223 -0
- package/docs/adr/0001-no-native-rust-lane.md +52 -0
- package/docs/codex-oauth-setup.md +14 -0
- package/docs/decisions/2026-06-10-git-history-bloat.md +61 -0
- package/docs/decisions/ADR-theme-dark-only-assets.md +43 -0
- package/docs/getting-started.md +1 -1
- package/docs/headroom-omk-final-summary.md +188 -0
- package/docs/headroom-omk-integration.md +394 -0
- package/docs/headroom-omk-setup-guide.md +422 -0
- package/docs/headroom-omk-usage-examples.md +371 -0
- package/docs/provider-maturity.md +1 -1
- package/docs/versioning.md +3 -3
- package/package.json +10 -10
- package/readmeasset/.npmignore +2 -0
- package/readmeasset/ASSET_INDEX.md +7 -5
- package/readmeasset/ASSET_PROVENANCE.md +21 -5
- package/readmeasset/omk-control-surfaces.svg +55 -0
- package/readmeasset/omk-core-loop.svg +1 -0
- package/readmeasset/omk-logo-mark.svg +1 -0
- package/readmeasset/omk-release-assertions.svg +51 -0
- package/dist/native/linux-x64/omk-safety +0 -0
- package/dist/util/native-safety.d.ts +0 -28
- package/dist/util/native-safety.js +0 -118
|
@@ -3,13 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Computes a release viability score R_v from ten normalized input
|
|
5
5
|
* dimensions and derives a verdict: block, pre-release, or stable.
|
|
6
|
+
*
|
|
7
|
+
* Weights are sourced from the omk.weights.v1 contract (releaseGate vector,
|
|
8
|
+
* normalize:true). The historical raw weights summed to 1.05; normalization
|
|
9
|
+
* divides weights, the regression penalty, and the verdict thresholds by the
|
|
10
|
+
* SAME factor (1/1.05), a pure uniform scaling — verdicts are mathematically
|
|
11
|
+
* identical for every input. Effective thresholds: preRelease ≈ 0.714286,
|
|
12
|
+
* stable ≈ 0.857143 (printed dynamically in reason strings).
|
|
6
13
|
*/
|
|
7
|
-
import {
|
|
14
|
+
import { releaseGateEffective } from "../runtime/weights-config.js";
|
|
8
15
|
/** Factory that creates the default release promotion gate. */
|
|
9
16
|
export function createReleasePromotionGate() {
|
|
17
|
+
const effective = releaseGateEffective();
|
|
18
|
+
const w = effective.weights;
|
|
19
|
+
const penalties = effective.penalties;
|
|
20
|
+
const preReleaseThreshold = effective.thresholds.preRelease;
|
|
21
|
+
const stableThreshold = effective.thresholds.stable;
|
|
10
22
|
return {
|
|
11
23
|
evaluate(inputs) {
|
|
12
|
-
const w = RELEASE_GATE_WEIGHTS;
|
|
13
24
|
const demoRun = inputs.demoRun ?? false;
|
|
14
25
|
const maturity = inputs.maturity ?? inputs.providerMinimum ?? 0;
|
|
15
26
|
const versionConsistency = inputs.versionConsistency ?? inputs.semver ?? 1;
|
|
@@ -25,7 +36,7 @@ export function createReleasePromotionGate() {
|
|
|
25
36
|
w.proof * inputs.proofMedian +
|
|
26
37
|
w.maturity * maturity +
|
|
27
38
|
w.docs * inputs.docs * versionConsistency -
|
|
28
|
-
|
|
39
|
+
penalties.regression * inputs.regressionSeverity;
|
|
29
40
|
const score = clamp01(rawScore);
|
|
30
41
|
const reasons = [];
|
|
31
42
|
const blocked = inputs.ci === 0 || inputs.freshInstallSmoke === 0 || versionConsistency === 0 || !demoRun;
|
|
@@ -48,20 +59,20 @@ export function createReleasePromotionGate() {
|
|
|
48
59
|
if (blocked) {
|
|
49
60
|
verdict = "block";
|
|
50
61
|
}
|
|
51
|
-
else if (score >=
|
|
62
|
+
else if (score >= stableThreshold && inputs.proofMedian >= 0.85 && maturity >= 0.80 && stableEligible) {
|
|
52
63
|
verdict = "stable";
|
|
53
|
-
reasons.push(`Score ${formatScore(score)} meets stable threshold (
|
|
64
|
+
reasons.push(`Score ${formatScore(score)} meets stable threshold (≥${stableThreshold.toFixed(2)}) with proof≥0.85, maturity≥0.80, live benchmark pass, sandbox violations=0, and exact-tag CI pass`);
|
|
54
65
|
}
|
|
55
|
-
else if (score >=
|
|
66
|
+
else if (score >= preReleaseThreshold && inputs.proofMedian >= 0.75) {
|
|
56
67
|
verdict = "pre-release";
|
|
57
|
-
reasons.push(`Score ${formatScore(score)} meets pre-release threshold (
|
|
58
|
-
if (score >=
|
|
68
|
+
reasons.push(`Score ${formatScore(score)} meets pre-release threshold (≥${preReleaseThreshold.toFixed(2)}) with proof≥0.75`);
|
|
69
|
+
if (score >= stableThreshold && inputs.proofMedian >= 0.85 && maturity >= 0.80 && !stableEligible) {
|
|
59
70
|
reasons.push("Stable verdict withheld until live benchmark passes, sandboxViolationCount is 0, and exact-tag CI passes");
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
else {
|
|
63
74
|
verdict = "block";
|
|
64
|
-
reasons.push(`Score ${formatScore(score)} below pre-release threshold (
|
|
75
|
+
reasons.push(`Score ${formatScore(score)} below pre-release threshold (≥${preReleaseThreshold.toFixed(2)}) or proof below 0.75`);
|
|
65
76
|
}
|
|
66
77
|
return {
|
|
67
78
|
score,
|
package/dist/cli/root.d.ts
CHANGED
package/dist/cli/root.js
CHANGED
|
@@ -3,6 +3,7 @@ import { t, initI18n } from "../util/i18n.js";
|
|
|
3
3
|
import { buildCustomHelp } from "../util/help-text.js";
|
|
4
4
|
const OMK_ENTRY_BRAND = "neon-grid";
|
|
5
5
|
const OMK_ENTRY_UI = "neon-grid";
|
|
6
|
+
export const OMK_ROOT_ENTRY_SURFACE = "omk-root-orchestrator";
|
|
6
7
|
export function buildRootChatLaunchArgs(options) {
|
|
7
8
|
const chatArgs = [
|
|
8
9
|
options.cliPath,
|
|
@@ -90,7 +91,7 @@ export function configureRootProgram(program, OMK_VERSION, OMK_VERSION_FOOTER) {
|
|
|
90
91
|
});
|
|
91
92
|
const childEnv = {
|
|
92
93
|
...process.env,
|
|
93
|
-
OMK_ENTRY_SURFACE:
|
|
94
|
+
OMK_ENTRY_SURFACE: OMK_ROOT_ENTRY_SURFACE,
|
|
94
95
|
};
|
|
95
96
|
const result = spawnSync(process.execPath, chatArgs, { stdio: "inherit", env: childEnv });
|
|
96
97
|
if (result.status && result.status !== 0) {
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI Theme — Barrel export for terminal capability, theme registry, and theme resolver.
|
|
3
3
|
*/
|
|
4
|
-
export { getTerminalCapability, defaultThemeForCapability, } from "./terminal-capability.js";
|
|
5
|
-
export type { TerminalCapability, ColorDepth, } from "./terminal-capability.js";
|
|
4
|
+
export { getTerminalCapability, defaultThemeForCapability, detectColorTier, colorTierForDepth, } from "./terminal-capability.js";
|
|
5
|
+
export type { TerminalCapability, ColorDepth, ColorTier, } from "./terminal-capability.js";
|
|
6
|
+
export { hexToOklab, hexToRgb255, normalizeHex, nearestXterm256, buildXterm256Lookup, } from "./oklab-quantize.js";
|
|
7
|
+
export type { OklabColor } from "./oklab-quantize.js";
|
|
8
|
+
export { compileTheme } from "./render-table.js";
|
|
9
|
+
export type { Ansi16Name, CompiledTheme, OmkThemeV1, RenderEntry, ThemeSemanticSpec, ThemeTokenKind, ThemeUsage, } from "./render-table.js";
|
|
6
10
|
export { getBuiltinTheme, listBuiltinThemes, registerBuiltinTheme, __registry, } from "./theme-registry.js";
|
|
7
11
|
export type { SemanticToken, ThemePalette, } from "./theme-registry.js";
|
|
8
12
|
export { resolveTheme } from "./theme-resolver.js";
|
|
9
13
|
export type { ResolveThemeOptions } from "./theme-resolver.js";
|
|
14
|
+
export { renderStatusFrame } from "./status-frame.js";
|
|
15
|
+
export { listThemeDocuments, loadThemeDocument, validateThemeDocument, } from "./theme-doc.js";
|
|
16
|
+
export type { ThemeDocumentRef } from "./theme-doc.js";
|
|
17
|
+
export { explainColorTier } from "./tier-explain.js";
|
|
18
|
+
export type { ColorTierExplanation } from "./tier-explain.js";
|
package/dist/cli/theme/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI Theme — Barrel export for terminal capability, theme registry, and theme resolver.
|
|
3
3
|
*/
|
|
4
|
-
export { getTerminalCapability, defaultThemeForCapability, } from "./terminal-capability.js";
|
|
4
|
+
export { getTerminalCapability, defaultThemeForCapability, detectColorTier, colorTierForDepth, } from "./terminal-capability.js";
|
|
5
|
+
export { hexToOklab, hexToRgb255, normalizeHex, nearestXterm256, buildXterm256Lookup, } from "./oklab-quantize.js";
|
|
6
|
+
export { compileTheme } from "./render-table.js";
|
|
5
7
|
export { getBuiltinTheme, listBuiltinThemes, registerBuiltinTheme, __registry, } from "./theme-registry.js";
|
|
6
8
|
export { resolveTheme } from "./theme-resolver.js";
|
|
9
|
+
export { renderStatusFrame } from "./status-frame.js";
|
|
10
|
+
export { listThemeDocuments, loadThemeDocument, validateThemeDocument, } from "./theme-doc.js";
|
|
11
|
+
export { explainColorTier } from "./tier-explain.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Theme — OKLab xterm-256 Quantizer
|
|
3
|
+
* Precomputes an OKLab nearest-neighbor lookup mapping 24-bit theme colors to
|
|
4
|
+
* xterm-256 indexes 16-255 ONLY. System colors 0-15 are never emitted because
|
|
5
|
+
* users commonly remap them, which would break theme contrast guarantees.
|
|
6
|
+
*
|
|
7
|
+
* Pure functions, no I/O. OKLab math is self-contained, ported from
|
|
8
|
+
* scripts/theme-check.mjs (Björn Ottosson sRGB → OKLab).
|
|
9
|
+
*/
|
|
10
|
+
export interface OklabColor {
|
|
11
|
+
readonly L: number;
|
|
12
|
+
readonly a: number;
|
|
13
|
+
readonly b: number;
|
|
14
|
+
}
|
|
15
|
+
/** Normalize "#aabbcc" / "AABBCC" to canonical "#AABBCC". Throws on bad input. */
|
|
16
|
+
export declare function normalizeHex(hex: string): string;
|
|
17
|
+
/** Parse a normalized 6-digit hex color into 0-255 RGB channels. */
|
|
18
|
+
export declare function hexToRgb255(hex: string): readonly [number, number, number];
|
|
19
|
+
export declare function hexToOklab(hex: string): OklabColor;
|
|
20
|
+
/**
|
|
21
|
+
* Nearest xterm-256 index (16-255 only) for a 24-bit hex color, measured by
|
|
22
|
+
* Euclidean distance in OKLab.
|
|
23
|
+
*/
|
|
24
|
+
export declare function nearestXterm256(hex: string): number;
|
|
25
|
+
/**
|
|
26
|
+
* Precompute a lookup table mapping each distinct theme color to its nearest
|
|
27
|
+
* xterm-256 index. Keys are normalized hex ("#AABBCC").
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildXterm256Lookup(hexes: readonly string[]): ReadonlyMap<string, number>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Theme — OKLab xterm-256 Quantizer
|
|
3
|
+
* Precomputes an OKLab nearest-neighbor lookup mapping 24-bit theme colors to
|
|
4
|
+
* xterm-256 indexes 16-255 ONLY. System colors 0-15 are never emitted because
|
|
5
|
+
* users commonly remap them, which would break theme contrast guarantees.
|
|
6
|
+
*
|
|
7
|
+
* Pure functions, no I/O. OKLab math is self-contained, ported from
|
|
8
|
+
* scripts/theme-check.mjs (Björn Ottosson sRGB → OKLab).
|
|
9
|
+
*/
|
|
10
|
+
function linearize(c) {
|
|
11
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
12
|
+
}
|
|
13
|
+
function channelsToOklab(r255, g255, b255) {
|
|
14
|
+
const r = linearize(r255 / 255);
|
|
15
|
+
const g = linearize(g255 / 255);
|
|
16
|
+
const b = linearize(b255 / 255);
|
|
17
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
18
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
19
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
20
|
+
return {
|
|
21
|
+
L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
22
|
+
a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
23
|
+
b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const HEX6_RE = /^#?[0-9A-Fa-f]{6}$/;
|
|
27
|
+
/** Normalize "#aabbcc" / "AABBCC" to canonical "#AABBCC". Throws on bad input. */
|
|
28
|
+
export function normalizeHex(hex) {
|
|
29
|
+
if (!HEX6_RE.test(hex)) {
|
|
30
|
+
throw new Error(`oklab-quantize: invalid 24-bit hex color "${hex}"`);
|
|
31
|
+
}
|
|
32
|
+
const v = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
33
|
+
return `#${v.toUpperCase()}`;
|
|
34
|
+
}
|
|
35
|
+
/** Parse a normalized 6-digit hex color into 0-255 RGB channels. */
|
|
36
|
+
export function hexToRgb255(hex) {
|
|
37
|
+
const v = normalizeHex(hex).slice(1);
|
|
38
|
+
return [
|
|
39
|
+
Number.parseInt(v.slice(0, 2), 16),
|
|
40
|
+
Number.parseInt(v.slice(2, 4), 16),
|
|
41
|
+
Number.parseInt(v.slice(4, 6), 16),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
export function hexToOklab(hex) {
|
|
45
|
+
const [r, g, b] = hexToRgb255(hex);
|
|
46
|
+
return channelsToOklab(r, g, b);
|
|
47
|
+
}
|
|
48
|
+
// xterm 6x6x6 color-cube channel levels (indexes 16-231).
|
|
49
|
+
const CUBE_LEVELS = [0, 95, 135, 175, 215, 255];
|
|
50
|
+
/** Build the OKLab palette for xterm indexes 16-255 (cube + grayscale ramp). */
|
|
51
|
+
function buildXtermPalette() {
|
|
52
|
+
const entries = [];
|
|
53
|
+
for (let r = 0; r < 6; r++) {
|
|
54
|
+
for (let g = 0; g < 6; g++) {
|
|
55
|
+
for (let b = 0; b < 6; b++) {
|
|
56
|
+
entries.push({
|
|
57
|
+
index: 16 + 36 * r + 6 * g + b,
|
|
58
|
+
lab: channelsToOklab(CUBE_LEVELS[r], CUBE_LEVELS[g], CUBE_LEVELS[b]),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Grayscale ramp: indexes 232-255 map to gray levels 8, 18, ..., 238.
|
|
64
|
+
for (let i = 0; i < 24; i++) {
|
|
65
|
+
const v = 8 + 10 * i;
|
|
66
|
+
entries.push({ index: 232 + i, lab: channelsToOklab(v, v, v) });
|
|
67
|
+
}
|
|
68
|
+
return entries;
|
|
69
|
+
}
|
|
70
|
+
// Precomputed once at module load — this is the "theme build/load time" cost.
|
|
71
|
+
const XTERM_PALETTE = buildXtermPalette();
|
|
72
|
+
/**
|
|
73
|
+
* Nearest xterm-256 index (16-255 only) for a 24-bit hex color, measured by
|
|
74
|
+
* Euclidean distance in OKLab.
|
|
75
|
+
*/
|
|
76
|
+
export function nearestXterm256(hex) {
|
|
77
|
+
const lab = hexToOklab(hex);
|
|
78
|
+
let bestIndex = 16;
|
|
79
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
80
|
+
for (const entry of XTERM_PALETTE) {
|
|
81
|
+
const dL = lab.L - entry.lab.L;
|
|
82
|
+
const da = lab.a - entry.lab.a;
|
|
83
|
+
const db = lab.b - entry.lab.b;
|
|
84
|
+
const dist = dL * dL + da * da + db * db;
|
|
85
|
+
if (dist < bestDist) {
|
|
86
|
+
bestDist = dist;
|
|
87
|
+
bestIndex = entry.index;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return bestIndex;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Precompute a lookup table mapping each distinct theme color to its nearest
|
|
94
|
+
* xterm-256 index. Keys are normalized hex ("#AABBCC").
|
|
95
|
+
*/
|
|
96
|
+
export function buildXterm256Lookup(hexes) {
|
|
97
|
+
const lookup = new Map();
|
|
98
|
+
for (const hex of hexes) {
|
|
99
|
+
const key = normalizeHex(hex);
|
|
100
|
+
if (!lookup.has(key)) {
|
|
101
|
+
lookup.set(key, nearestXterm256(key));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return lookup;
|
|
105
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Theme — Render Table Compiler
|
|
3
|
+
* Compiles an omk.theme.v1 document (themes/*.theme.json) into per-surface
|
|
4
|
+
* render tables at startup. For each semantic token it precomputes the SGR
|
|
5
|
+
* sequence for the requested degradation tier:
|
|
6
|
+
* truecolor → SGR 38;2;R;G;B
|
|
7
|
+
* 256 → SGR 38;5;N via the OKLab nearest-neighbor lookup (16-255 only)
|
|
8
|
+
* 16 → the theme's hand-authored ansi16 (fallback16) mapping
|
|
9
|
+
* no-color → empty string
|
|
10
|
+
* Single entrypoint: compileTheme(theme, tier).
|
|
11
|
+
*/
|
|
12
|
+
import type { ColorTier } from "./terminal-capability.js";
|
|
13
|
+
export type ThemeUsage = "text" | "indicator" | "background";
|
|
14
|
+
export type ThemeTokenKind = "state" | "chrome" | "background";
|
|
15
|
+
export type Ansi16Name = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "brightBlack" | "brightRed" | "brightGreen" | "brightYellow" | "brightBlue" | "brightMagenta" | "brightCyan" | "brightWhite";
|
|
16
|
+
export interface ThemeSemanticSpec {
|
|
17
|
+
readonly color: string;
|
|
18
|
+
readonly glyph?: string;
|
|
19
|
+
readonly kind?: ThemeTokenKind;
|
|
20
|
+
readonly usage: ThemeUsage;
|
|
21
|
+
}
|
|
22
|
+
export interface OmkThemeV1 {
|
|
23
|
+
readonly schemaVersion: "omk.theme.v1";
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly displayName?: string;
|
|
26
|
+
readonly mode: "dark" | "light";
|
|
27
|
+
readonly primitives: Readonly<Record<string, string>>;
|
|
28
|
+
readonly backgrounds: readonly string[];
|
|
29
|
+
readonly semantics: Readonly<Record<string, ThemeSemanticSpec>>;
|
|
30
|
+
readonly components: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
31
|
+
readonly fallback16: Readonly<Record<string, string>>;
|
|
32
|
+
}
|
|
33
|
+
export interface RenderEntry {
|
|
34
|
+
/** Semantic role this entry resolves (e.g. "dag.lane.running"). */
|
|
35
|
+
readonly role: string;
|
|
36
|
+
/** Canonical 24-bit hex of the underlying primitive. */
|
|
37
|
+
readonly hex: string;
|
|
38
|
+
/** Mandatory state glyph, or "" for chrome/background tokens. */
|
|
39
|
+
readonly glyph: string;
|
|
40
|
+
/** Precomputed SGR open sequence for the compiled tier ("" on no-color). */
|
|
41
|
+
readonly sgr: string;
|
|
42
|
+
}
|
|
43
|
+
export interface CompiledTheme {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly tier: ColorTier;
|
|
46
|
+
/** SGR reset for this tier ("" on no-color). */
|
|
47
|
+
readonly reset: string;
|
|
48
|
+
/** Semantic role → precomputed render entry. */
|
|
49
|
+
readonly tokens: Readonly<Record<string, RenderEntry>>;
|
|
50
|
+
/** Component surface → slot → precomputed render entry. */
|
|
51
|
+
readonly surfaces: Readonly<Record<string, Readonly<Record<string, RenderEntry>>>>;
|
|
52
|
+
/** Wrap text in the precomputed sequence for a semantic role. */
|
|
53
|
+
readonly paint: (role: string, text: string) => string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Compile an omk.theme.v1 document into a per-surface render table for one
|
|
57
|
+
* degradation tier. Pure: computes everything up front, performs no I/O.
|
|
58
|
+
*/
|
|
59
|
+
export declare function compileTheme(theme: OmkThemeV1, tier: ColorTier): CompiledTheme;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Theme — Render Table Compiler
|
|
3
|
+
* Compiles an omk.theme.v1 document (themes/*.theme.json) into per-surface
|
|
4
|
+
* render tables at startup. For each semantic token it precomputes the SGR
|
|
5
|
+
* sequence for the requested degradation tier:
|
|
6
|
+
* truecolor → SGR 38;2;R;G;B
|
|
7
|
+
* 256 → SGR 38;5;N via the OKLab nearest-neighbor lookup (16-255 only)
|
|
8
|
+
* 16 → the theme's hand-authored ansi16 (fallback16) mapping
|
|
9
|
+
* no-color → empty string
|
|
10
|
+
* Single entrypoint: compileTheme(theme, tier).
|
|
11
|
+
*/
|
|
12
|
+
import { buildXterm256Lookup, normalizeHex, hexToRgb255 } from "./oklab-quantize.js";
|
|
13
|
+
const ESC = "\u001b[";
|
|
14
|
+
const SGR_RESET = `${ESC}0m`;
|
|
15
|
+
// Foreground SGR codes for the 16-color tier (standard 30-37, bright 90-97).
|
|
16
|
+
const ANSI16_FG = {
|
|
17
|
+
black: 30,
|
|
18
|
+
red: 31,
|
|
19
|
+
green: 32,
|
|
20
|
+
yellow: 33,
|
|
21
|
+
blue: 34,
|
|
22
|
+
magenta: 35,
|
|
23
|
+
cyan: 36,
|
|
24
|
+
white: 37,
|
|
25
|
+
brightBlack: 90,
|
|
26
|
+
brightRed: 91,
|
|
27
|
+
brightGreen: 92,
|
|
28
|
+
brightYellow: 93,
|
|
29
|
+
brightBlue: 94,
|
|
30
|
+
brightMagenta: 95,
|
|
31
|
+
brightCyan: 96,
|
|
32
|
+
brightWhite: 97,
|
|
33
|
+
};
|
|
34
|
+
function isAnsi16Name(name) {
|
|
35
|
+
return Object.prototype.hasOwnProperty.call(ANSI16_FG, name);
|
|
36
|
+
}
|
|
37
|
+
function resolvePrimitiveHex(theme, role, color) {
|
|
38
|
+
const raw = theme.primitives[color];
|
|
39
|
+
if (raw === undefined) {
|
|
40
|
+
throw new Error(`compileTheme(${theme.name}): semantic "${role}" references unknown primitive "${color}"`);
|
|
41
|
+
}
|
|
42
|
+
return normalizeHex(raw);
|
|
43
|
+
}
|
|
44
|
+
function sgrForTier(theme, role, hex, tier, lookup256) {
|
|
45
|
+
switch (tier) {
|
|
46
|
+
case "truecolor": {
|
|
47
|
+
const [r, g, b] = hexToRgb255(hex);
|
|
48
|
+
return `${ESC}38;2;${r};${g};${b}m`;
|
|
49
|
+
}
|
|
50
|
+
case "256": {
|
|
51
|
+
const index = lookup256.get(hex);
|
|
52
|
+
if (index === undefined) {
|
|
53
|
+
throw new Error(`compileTheme(${theme.name}): missing 256-color lookup for "${role}" (${hex})`);
|
|
54
|
+
}
|
|
55
|
+
return `${ESC}38;5;${index}m`;
|
|
56
|
+
}
|
|
57
|
+
case "16": {
|
|
58
|
+
const name = theme.fallback16[role];
|
|
59
|
+
if (name === undefined || !isAnsi16Name(name)) {
|
|
60
|
+
throw new Error(`compileTheme(${theme.name}): semantic "${role}" has no valid ansi16 fallback (got "${name ?? "<missing>"}")`);
|
|
61
|
+
}
|
|
62
|
+
return `${ESC}${ANSI16_FG[name]}m`;
|
|
63
|
+
}
|
|
64
|
+
case "no-color":
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Compile an omk.theme.v1 document into a per-surface render table for one
|
|
70
|
+
* degradation tier. Pure: computes everything up front, performs no I/O.
|
|
71
|
+
*/
|
|
72
|
+
export function compileTheme(theme, tier) {
|
|
73
|
+
// Precompute the OKLab → xterm-256 lookup once per compile (build/load time).
|
|
74
|
+
const distinctHexes = Object.entries(theme.semantics).map(([role, spec]) => resolvePrimitiveHex(theme, role, spec.color));
|
|
75
|
+
const lookup256 = tier === "256" ? buildXterm256Lookup(distinctHexes) : new Map();
|
|
76
|
+
const tokens = {};
|
|
77
|
+
for (const [role, spec] of Object.entries(theme.semantics)) {
|
|
78
|
+
const hex = resolvePrimitiveHex(theme, role, spec.color);
|
|
79
|
+
tokens[role] = {
|
|
80
|
+
role,
|
|
81
|
+
hex,
|
|
82
|
+
glyph: spec.glyph ?? "",
|
|
83
|
+
sgr: sgrForTier(theme, role, hex, tier, lookup256),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const surfaces = {};
|
|
87
|
+
for (const [surface, slots] of Object.entries(theme.components)) {
|
|
88
|
+
const compiled = {};
|
|
89
|
+
for (const [slot, role] of Object.entries(slots)) {
|
|
90
|
+
const entry = tokens[role];
|
|
91
|
+
if (entry === undefined) {
|
|
92
|
+
throw new Error(`compileTheme(${theme.name}): component ${surface}.${slot} references unknown semantic role "${role}"`);
|
|
93
|
+
}
|
|
94
|
+
compiled[slot] = entry;
|
|
95
|
+
}
|
|
96
|
+
surfaces[surface] = compiled;
|
|
97
|
+
}
|
|
98
|
+
const reset = tier === "no-color" ? "" : SGR_RESET;
|
|
99
|
+
return {
|
|
100
|
+
name: theme.name,
|
|
101
|
+
tier,
|
|
102
|
+
reset,
|
|
103
|
+
tokens,
|
|
104
|
+
surfaces,
|
|
105
|
+
paint(role, text) {
|
|
106
|
+
const entry = tokens[role];
|
|
107
|
+
if (entry === undefined || entry.sgr === "")
|
|
108
|
+
return text;
|
|
109
|
+
return entry.sgr + text + reset;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Theme — Representative Status Frame
|
|
3
|
+
* Renders the ONE representative TUI status frame shared by the four-tier
|
|
4
|
+
* degradation snapshots (test/theme-degradation.test.mjs) and the
|
|
5
|
+
* `omk theme preview <name>` CLI surface. Keeping a single renderer
|
|
6
|
+
* guarantees the CLI preview shows exactly the snapshot-gated frame.
|
|
7
|
+
*/
|
|
8
|
+
import type { CompiledTheme } from "./render-table.js";
|
|
9
|
+
/** One representative TUI status frame built from theme tokens + glyphs. */
|
|
10
|
+
export declare function renderStatusFrame(ct: CompiledTheme): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Theme — Representative Status Frame
|
|
3
|
+
* Renders the ONE representative TUI status frame shared by the four-tier
|
|
4
|
+
* degradation snapshots (test/theme-degradation.test.mjs) and the
|
|
5
|
+
* `omk theme preview <name>` CLI surface. Keeping a single renderer
|
|
6
|
+
* guarantees the CLI preview shows exactly the snapshot-gated frame.
|
|
7
|
+
*/
|
|
8
|
+
function paintWithGlyph(ct, role, text) {
|
|
9
|
+
const entry = ct.tokens[role];
|
|
10
|
+
const glyph = entry?.glyph ?? "";
|
|
11
|
+
return ct.paint(role, glyph === "" ? text : `${glyph} ${text}`);
|
|
12
|
+
}
|
|
13
|
+
/** One representative TUI status frame built from theme tokens + glyphs. */
|
|
14
|
+
export function renderStatusFrame(ct) {
|
|
15
|
+
const p = (role, text) => paintWithGlyph(ct, role, text);
|
|
16
|
+
return [
|
|
17
|
+
`${p("control.accent", "OMK//CONTROL")} ${p("control.dim", "night-city ops console")}`,
|
|
18
|
+
`${p("dag.lane.running", "lane compile")} ${p("dag.lane.done", "lane schema")} ${p("dag.lane.queued", "lane docs")}`,
|
|
19
|
+
`${p("evidence.pass", "contrast 48/48")} ${p("evidence.pending", "snapshots")} ${p("route.fallback", "provider kimi")}`,
|
|
20
|
+
`${p("telemetry.warn", "headroom 81%")} ${p("control.fg", "tier ready")}`,
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
@@ -3,15 +3,20 @@
|
|
|
3
3
|
* Detects color support, unicode, TTY status, and terminal dimensions.
|
|
4
4
|
*/
|
|
5
5
|
export type ColorDepth = 0 | 1 | 4 | 8 | 24;
|
|
6
|
+
/** Degradation tier used by the render-table compiler. */
|
|
7
|
+
export type ColorTier = "truecolor" | "256" | "16" | "no-color";
|
|
6
8
|
export interface TerminalCapability {
|
|
7
9
|
readonly isTty: boolean;
|
|
8
10
|
readonly isCi: boolean;
|
|
9
11
|
readonly colorDisabled: boolean;
|
|
10
12
|
readonly colorDepth: ColorDepth;
|
|
13
|
+
readonly colorTier: ColorTier;
|
|
11
14
|
readonly unicode: boolean;
|
|
12
15
|
readonly width: number;
|
|
13
16
|
}
|
|
14
|
-
export declare function
|
|
17
|
+
export declare function colorTierForDepth(depth: ColorDepth): ColorTier;
|
|
18
|
+
export declare function detectColorTier(argv?: readonly string[]): ColorTier;
|
|
19
|
+
export declare function getTerminalCapability(argv?: readonly string[]): TerminalCapability;
|
|
15
20
|
export declare function defaultThemeForCapability(cap: TerminalCapability): {
|
|
16
21
|
name: string;
|
|
17
22
|
mode: "dark" | "light" | "auto" | "mono";
|
|
@@ -14,8 +14,11 @@ function detectCi() {
|
|
|
14
14
|
|| process.env.APPVEYOR
|
|
15
15
|
|| process.env.JENKINS_URL);
|
|
16
16
|
}
|
|
17
|
-
function detectColorDepth() {
|
|
18
|
-
// Explicit opt-out
|
|
17
|
+
function detectColorDepth(argv = process.argv) {
|
|
18
|
+
// Explicit opt-out: --no-color CLI flag or NO_COLOR env
|
|
19
|
+
if (argv.includes("--no-color")) {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
19
22
|
if (process.env.NO_COLOR !== undefined || process.env.TERM === "dumb") {
|
|
20
23
|
return 0;
|
|
21
24
|
}
|
|
@@ -65,14 +68,27 @@ function getTerminalWidth() {
|
|
|
65
68
|
}
|
|
66
69
|
return 80;
|
|
67
70
|
}
|
|
68
|
-
export function
|
|
71
|
+
export function colorTierForDepth(depth) {
|
|
72
|
+
if (depth === 24)
|
|
73
|
+
return "truecolor";
|
|
74
|
+
if (depth === 8)
|
|
75
|
+
return "256";
|
|
76
|
+
if (depth === 4 || depth === 1)
|
|
77
|
+
return "16";
|
|
78
|
+
return "no-color";
|
|
79
|
+
}
|
|
80
|
+
export function detectColorTier(argv = process.argv) {
|
|
81
|
+
return colorTierForDepth(detectColorDepth(argv));
|
|
82
|
+
}
|
|
83
|
+
export function getTerminalCapability(argv = process.argv) {
|
|
69
84
|
const isCi = detectCi();
|
|
70
|
-
const colorDepth = detectColorDepth();
|
|
85
|
+
const colorDepth = detectColorDepth(argv);
|
|
71
86
|
return {
|
|
72
87
|
isTty: process.stdout.isTTY ?? false,
|
|
73
88
|
isCi,
|
|
74
89
|
colorDisabled: colorDepth === 0,
|
|
75
90
|
colorDepth,
|
|
91
|
+
colorTier: colorTierForDepth(colorDepth),
|
|
76
92
|
unicode: detectUnicode(),
|
|
77
93
|
width: getTerminalWidth(),
|
|
78
94
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Theme — omk.theme.v1 Document Loader
|
|
3
|
+
* Discovers, loads, and structurally validates themes/*.theme.json documents
|
|
4
|
+
* (the executable twin of schemas/omk.theme.v1.schema.json, mirroring
|
|
5
|
+
* scripts/theme-check.mjs validateTheme()). Read-only filesystem access.
|
|
6
|
+
*/
|
|
7
|
+
import type { OmkThemeV1 } from "./render-table.js";
|
|
8
|
+
export interface ThemeDocumentRef {
|
|
9
|
+
/** Theme name derived from the file name (e.g. "night-city"). */
|
|
10
|
+
readonly name: string;
|
|
11
|
+
/** Absolute path of the .theme.json document. */
|
|
12
|
+
readonly path: string;
|
|
13
|
+
}
|
|
14
|
+
/** List omk.theme.v1 documents discoverable from cwd (or the package root). */
|
|
15
|
+
export declare function listThemeDocuments(cwd?: string): readonly ThemeDocumentRef[];
|
|
16
|
+
/** Load one theme document by name. Returns undefined when absent/unreadable. */
|
|
17
|
+
export declare function loadThemeDocument(name: string, cwd?: string): OmkThemeV1 | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Structural validation of an omk.theme.v1 document — the same executable
|
|
20
|
+
* rules scripts/theme-check.mjs enforces in CI. Returns [] when valid.
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateThemeDocument(theme: OmkThemeV1): readonly string[];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Theme — omk.theme.v1 Document Loader
|
|
3
|
+
* Discovers, loads, and structurally validates themes/*.theme.json documents
|
|
4
|
+
* (the executable twin of schemas/omk.theme.v1.schema.json, mirroring
|
|
5
|
+
* scripts/theme-check.mjs validateTheme()). Read-only filesystem access.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
const ANSI16_NAMES = new Set([
|
|
11
|
+
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
|
|
12
|
+
"brightBlack", "brightRed", "brightGreen", "brightYellow",
|
|
13
|
+
"brightBlue", "brightMagenta", "brightCyan", "brightWhite",
|
|
14
|
+
]);
|
|
15
|
+
/** Candidate themes/ directories: project cwd first, then the package root. */
|
|
16
|
+
function themesDirCandidates(cwd) {
|
|
17
|
+
const moduleRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
18
|
+
const candidates = [join(cwd, "themes"), join(moduleRoot, "themes")];
|
|
19
|
+
return [...new Set(candidates)];
|
|
20
|
+
}
|
|
21
|
+
/** List omk.theme.v1 documents discoverable from cwd (or the package root). */
|
|
22
|
+
export function listThemeDocuments(cwd = process.cwd()) {
|
|
23
|
+
for (const dir of themesDirCandidates(cwd)) {
|
|
24
|
+
if (!existsSync(dir))
|
|
25
|
+
continue;
|
|
26
|
+
const refs = readdirSync(dir)
|
|
27
|
+
.filter((f) => f.endsWith(".theme.json"))
|
|
28
|
+
.map((f) => ({ name: f.replace(/\.theme\.json$/, ""), path: join(dir, f) }));
|
|
29
|
+
if (refs.length > 0)
|
|
30
|
+
return refs;
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
/** Load one theme document by name. Returns undefined when absent/unreadable. */
|
|
35
|
+
export function loadThemeDocument(name, cwd = process.cwd()) {
|
|
36
|
+
const ref = listThemeDocuments(cwd).find((r) => r.name === name);
|
|
37
|
+
if (ref === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(ref.path, "utf8"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Structural validation of an omk.theme.v1 document — the same executable
|
|
48
|
+
* rules scripts/theme-check.mjs enforces in CI. Returns [] when valid.
|
|
49
|
+
*/
|
|
50
|
+
export function validateThemeDocument(theme) {
|
|
51
|
+
const errors = [];
|
|
52
|
+
const err = (m) => { errors.push(m); };
|
|
53
|
+
if (theme.schemaVersion !== "omk.theme.v1")
|
|
54
|
+
err(`schemaVersion must be "omk.theme.v1"`);
|
|
55
|
+
if (!/^[a-z][a-z0-9-]*$/.test(theme.name ?? ""))
|
|
56
|
+
err("invalid theme name");
|
|
57
|
+
if (theme.mode !== "dark" && theme.mode !== "light")
|
|
58
|
+
err("mode must be dark|light");
|
|
59
|
+
const primitives = theme.primitives ?? {};
|
|
60
|
+
for (const [k, v] of Object.entries(primitives)) {
|
|
61
|
+
if (!/^#[0-9A-Fa-f]{6}$/.test(v))
|
|
62
|
+
err(`primitive ${k}: bad hex ${v}`);
|
|
63
|
+
}
|
|
64
|
+
for (const bg of theme.backgrounds ?? []) {
|
|
65
|
+
if (primitives[bg] === undefined)
|
|
66
|
+
err(`background "${bg}" is not a primitive`);
|
|
67
|
+
}
|
|
68
|
+
const semantics = theme.semantics ?? {};
|
|
69
|
+
for (const [role, spec] of Object.entries(semantics)) {
|
|
70
|
+
if (primitives[spec.color] === undefined)
|
|
71
|
+
err(`semantic ${role}: color "${spec.color}" is not a primitive`);
|
|
72
|
+
const kind = spec.kind ?? "state";
|
|
73
|
+
if (kind === "state" && (spec.glyph === undefined || spec.glyph === "")) {
|
|
74
|
+
err(`semantic state ${role}: missing mandatory glyph`);
|
|
75
|
+
}
|
|
76
|
+
if (!["text", "indicator", "background"].includes(spec.usage))
|
|
77
|
+
err(`semantic ${role}: bad usage`);
|
|
78
|
+
}
|
|
79
|
+
for (const [comp, slots] of Object.entries(theme.components ?? {})) {
|
|
80
|
+
for (const [slot, role] of Object.entries(slots)) {
|
|
81
|
+
if (semantics[role] === undefined)
|
|
82
|
+
err(`component ${comp}.${slot}: unknown semantic role "${role}"`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const fb = theme.fallback16 ?? {};
|
|
86
|
+
for (const role of Object.keys(semantics)) {
|
|
87
|
+
if (fb[role] === undefined)
|
|
88
|
+
err(`fallback16: missing entry for semantic role "${role}"`);
|
|
89
|
+
}
|
|
90
|
+
for (const [role, name] of Object.entries(fb)) {
|
|
91
|
+
if (!ANSI16_NAMES.has(name))
|
|
92
|
+
err(`fallback16 ${role}: unknown ANSI-16 name "${name}"`);
|
|
93
|
+
if (semantics[role] === undefined)
|
|
94
|
+
err(`fallback16 ${role}: no such semantic role`);
|
|
95
|
+
}
|
|
96
|
+
return errors;
|
|
97
|
+
}
|