diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c13b3f..497c186 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: run: tests/refresh-fixtures.sh - name: Fail if the vendored copies drifted run: | - git diff --exit-code -- tests/fixtures || { - echo "::error::A hook contract moved in lich. Run tests/refresh-fixtures.sh, then make the scripts match." + git diff --exit-code -- tests/fixtures skills/theme/template.json || { + echo "::error::A hook contract or the theme template moved in lich. Run tests/refresh-fixtures.sh, then make the scripts (or the theme skill) match." exit 1 } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a04bd5..f37eab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **The theme validator knows the status tones.** `validate.mjs` read its + tokens off a template copied before lich 0.53.0 made `tone-pass` and + `tone-wait` required, so it called a current theme's tones unknown and passed + a theme without them that lich rejects on import. The template is now + refreshed from lich at `tests/lich-ref` with the contract fixtures, and CI + fails when the copy drifts; the skill lists the two tokens. + ## [0.18.1] - 2026-10-08 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 0ae7379..138bdc4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,7 @@ Each doc carries the event mapping for **every** provider, one column each — a ## Skills -- [skills/theme](skills/theme/SKILL.md) — writing, porting and validating lich color themes, as a single file or as a versioned theme repository. Mirrors the theme contract in the lich repository (`docs/themes.md` there); `template.json` is a copy of the starter lich itself hands out, and `validate.mjs` derives its token sets from that copy. A change to the theme shape, or to the repository contract, lands here too. +- [skills/theme](skills/theme/SKILL.md) — writing, porting and validating lich color themes, as a single file or as a versioned theme repository. Mirrors the theme contract in the lich repository (`docs/themes.md` there); `template.json` is a copy of the starter lich itself hands out, vendored at `tests/lich-ref` by `tests/refresh-fixtures.sh` and diffed by CI like the fixtures, and `validate.mjs` derives its token sets from that copy. A change to the theme shape, or to the repository contract, lands here too. ## Tests diff --git a/skills/theme/SKILL.md b/skills/theme/SKILL.md index be27031..bae48fd 100644 --- a/skills/theme/SKILL.md +++ b/skills/theme/SKILL.md @@ -122,7 +122,7 @@ manifest**, commit, push. The user takes it with **Update** on the theme's row. `com1`–`com9`, `lpt1`–`lpt9`) — the id names a file. - `name`: required, non-blank, ≤ 128 characters. - `scheme`: `light` or `dark`. Nothing else. -- `app`: **every one of the 31 tokens** must be present. An unknown token is an +- `app`: **every one of the 33 tokens** must be present. An unknown token is an error, not a warning. - `terminal`: `background` and `foreground` required; the other 20 tokens optional. Unknown tokens rejected. @@ -199,6 +199,7 @@ pair contrasty, they are never used apart): | `muted` / `muted-foreground` | secondary text, resting icons, paths, meta | | `primary` / `primary-foreground` | the single high-emphasis button fill, and the Switch when on | | `destructive` | destructive actions and diff removals — must stay legible as red | +| `tone-pass` / `tone-wait` | status tones: a pass (green) and a wait (amber) on session icons, pull request checks and reviews. Drawn as text and icons on `background`/`card`, so they need contrast there. Required since lich 0.53.0 — a pack that carries them is one an older lich rejects as unknown tokens, which is what `minLichVersion: "0.53.0"` names | | `border` | hairline seams. Translucent works well in dark (`oklch(1 0 0 / 10%)`) | | `input` | input and control edges, usually a touch stronger than `border` | | `ring` | focus ring, always visible | diff --git a/skills/theme/template.json b/skills/theme/template.json index ee2f528..df3e3d3 100644 --- a/skills/theme/template.json +++ b/skills/theme/template.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/omartelo/lich/main/themes/lich-theme.schema.json", "id": "purple-night", "name": "Purple Night", "scheme": "dark", @@ -18,6 +19,8 @@ "accent": "#3a2d57", "accent-foreground": "#f4efff", "destructive": "#ff5c8a", + "tone-pass": "oklch(0.696 0.17 162.48)", + "tone-wait": "oklch(0.769 0.188 70.08)", "border": "#4a3a68", "input": "#554178", "ring": "#c4a7ff", diff --git a/tests/refresh-fixtures.sh b/tests/refresh-fixtures.sh index 3210005..a7e7a95 100755 --- a/tests/refresh-fixtures.sh +++ b/tests/refresh-fixtures.sh @@ -1,5 +1,6 @@ #!/bin/sh -# Refreshes the vendored copies of lich's hook-contract fixtures. +# Refreshes the vendored copies of lich's hook-contract fixtures, and of the +# theme template the theme skill hands out and validates against. # # The fixtures are canonical in the lich repository (docs/hooks/fixtures/ # there). This repository only vendors them so the test suite never needs the @@ -15,7 +16,8 @@ set -eu here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) ref=$(tr -d '[:space:]' <"$here/lich-ref") -base=https://raw.githubusercontent.com/omartelo/lich/$ref/docs/hooks/fixtures +raw=https://raw.githubusercontent.com/omartelo/lich/$ref +base=$raw/docs/hooks/fixtures for file in session-state.jsonl session-start.jsonl session-title.jsonl \ session-touched.jsonl mod-usage.jsonl mod-control.jsonl mod-commands.json \ @@ -24,3 +26,7 @@ for file in session-state.jsonl session-start.jsonl session-title.jsonl \ echo "refreshed $file at $ref" done +# validate.mjs reads its token sets off this copy, so a token lich adds reaches +# the validator by moving tests/lich-ref, not by hand. +curl -fsSL "$raw/themes/template.json" -o "$here/../skills/theme/template.json" +echo "refreshed skills/theme/template.json at $ref" diff --git a/tests/theme-validate.test.mjs b/tests/theme-validate.test.mjs new file mode 100644 index 0000000..11a1853 --- /dev/null +++ b/tests/theme-validate.test.mjs @@ -0,0 +1,41 @@ +// Pins skills/theme/validate.mjs against the app tokens lich requires on +// import: a theme it calls ok must be one lich installs, and the other way round. +// +// Run: node --test tests/ + +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const validator = new URL('../skills/theme/validate.mjs', import.meta.url).pathname +const template = JSON.parse( + readFileSync(new URL('../skills/theme/template.json', import.meta.url), 'utf8'), +) + +// The status tones lich 0.53.0 made required (omartelo/lich#617), with the +// bundled Dark theme's values. +const tones = { + 'tone-pass': 'oklch(0.696 0.17 162.48)', + 'tone-wait': 'oklch(0.769 0.188 70.08)', +} + +function validate(app) { + const path = join(mkdtempSync(join(tmpdir(), 'lich-theme-')), 'theme.json') + writeFileSync(path, JSON.stringify({ ...template, id: 'tones', app })) + return spawnSync(process.execPath, [validator, path], { encoding: 'utf8' }) +} + +test('a theme carrying the status tones passes', () => { + const run = validate({ ...template.app, ...tones }) + assert.equal(run.status, 0, run.stderr) +}) + +test('a theme missing a status tone is rejected the way lich rejects it', () => { + const { 'tone-wait': _, ...app } = { ...template.app, ...tones } + const run = validate(app) + assert.equal(run.status, 1) + assert.match(run.stderr, /app\.tone-wait is required/) +})