@lemoncode/lemony 0.5.0 → 0.5.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/catalog/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.0
1
+ 0.5.1
@@ -18,7 +18,9 @@ Designer; follow the steps there.
18
18
  deletes tool-only variables). Preview the plan, push on confirmation, then record the drift
19
19
  baseline only after the push succeeds.
20
20
  - _empty_ — check the drift state (`lemony status`) and, if an export is pending and the tool
21
- is connected, offer `export`; otherwise report the current state.
21
+ is connected, offer `export`; otherwise report the current state. `status` shows no drift
22
+ line both when there is no token file and when the file cannot be read or fails
23
+ validation — tell the two apart with `lemony doctor`'s `design-tool-drift` check.
22
24
 
23
25
  The design tool is a **projection** of the canonical JSON, never a peer source of truth.
24
26
  Detect the tool at runtime: read the `com.lemony.design-tool` binding at the root of
@@ -46,8 +46,17 @@ WARNINGS=()
46
46
  # awk/grep/git (preinstalled) cover the rest.
47
47
  CONFIG_VERSION=""
48
48
  CONFIG_REPO=""
49
- if [ ! -f "$CONFIG" ]; then
49
+ if [ ! -e "$CONFIG" ]; then
50
50
  ERRORS+=("harness.config.yml not found at $REPO_ROOT. Run \`lemony install\`.")
51
+ elif [ ! -f "$CONFIG" ]; then
52
+ # There, but not a file awk can read: a directory, or a FIFO it would block on
53
+ # until something wrote to it. `-e`/`-f` follow a symlink, so a dead link stays
54
+ # "not found" above and a link to one of these lands here.
55
+ ERRORS+=("harness.config.yml at $REPO_ROOT is not a regular file (a directory, a FIFO, a socket or a device), so it was not read. Replace it with the config file.")
56
+ elif [ ! -r "$CONFIG" ]; then
57
+ # A file awk cannot open failed the key check below, so the boot blamed missing keys
58
+ # under a raw awk error.
59
+ ERRORS+=("harness.config.yml at $REPO_ROOT cannot be read (check its permissions), so it was not read.")
51
60
  elif ! awk '
52
61
  { sub(/\r$/, "") }
53
62
  /^[^[:space:]#]/ {
@@ -190,6 +199,19 @@ if [ "${#ERRORS[@]}" -eq 0 ] && [ -n "$GIT_USER_EMAIL" ]; then
190
199
  CURRENT_PATH="$REPO_ROOT/.claude/state/current-$USER_SLUG.md"
191
200
  if [ -z "$USER_SLUG" ]; then
192
201
  : # path-unsafe slug — pointer write skipped above
202
+ elif [ -L "$REPO_ROOT/.claude" ] || [ -L "$REPO_ROOT/.claude/state" ]; then
203
+ # A linked directory takes the pointer, and every refresh after it, out of the
204
+ # repo — the same write-through the dangling-link branch below refuses.
205
+ WARNINGS+=(".claude or .claude/state is a symlink, so the session start was not recorded (the pointer would be written wherever it points). Replace it with a directory.")
206
+ elif [ -e "$CURRENT_PATH" ] && [ ! -f "$CURRENT_PATH" ]; then
207
+ # There, but not a file: writing to a FIFO blocks until something reads it, and
208
+ # awk reading one blocks until something writes. The pointer is not critical, so
209
+ # the boot goes on without it — a warning, not a blocking error.
210
+ WARNINGS+=(".claude/state/current-$USER_SLUG.md is not a regular file (a directory, a FIFO, a socket or a device), so the session start was not recorded. Remove it; the next session recreates it.")
211
+ elif [ -L "$CURRENT_PATH" ] && [ ! -e "$CURRENT_PATH" ]; then
212
+ # A symlink whose target is gone: `-e`/`-f` follow it and see nothing, and the
213
+ # `cat >` below would create the target wherever the link points.
214
+ WARNINGS+=(".claude/state/current-$USER_SLUG.md is a symlink whose target is missing, so the session start was not recorded (writing through it would create the file it points at). Remove it; the next session recreates it.")
193
215
  elif [ ! -f "$CURRENT_PATH" ]; then
194
216
  mkdir -p "$REPO_ROOT/.claude/state"
195
217
  cat > "$CURRENT_PATH" <<EOF
@@ -309,12 +309,13 @@ fi
309
309
  # same recipe as playbook-scan.sh): emit `<key>\t<value>` for the uncommented
310
310
  # `checks_timeout_secs` / `allow_no_checks` entries — inline comments stripped
311
311
  # (on the block header too) and surrounding quotes removed. Any column-0 line
312
- # ends the block. Absent/unreadable config → baked defaults.
312
+ # ends the block. Absent/unreadable/non-regular config → baked defaults.
313
313
  CONFIG_TIMEOUT=""
314
314
  CONFIG_ALLOW_NO_CHECKS=""
315
315
  read_merge_config() {
316
316
  local config="$ROOT/harness.config.yml"
317
- [ -r "$config" ] || return 0
317
+ # `-f` as well as `-r`: a FIFO is readable, and awk on it would hang the merge.
318
+ { [ -f "$config" ] && [ -r "$config" ]; } || return 0
318
319
 
319
320
  local key value
320
321
  while IFS=$'\t' read -r key value; do
@@ -39,7 +39,8 @@
39
39
  # which would breach the per-fire budget) and with no materialized state file to
40
40
  # go stale before an `update` command exists (P7). Falls back to the baked
41
41
  # defaults when the config, its `paths` block, or a key is absent or commented
42
- # out (fresh clone, the vendor repo dogfooding itself, an un-customized install).
42
+ # out (fresh clone, the vendor repo dogfooding itself, an un-customized install),
43
+ # and when the config is not a regular file.
43
44
  _resolve_playbook_dirs() {
44
45
  local repo_root="$1"
45
46
  local home_dir="$2"
@@ -47,7 +48,8 @@ _resolve_playbook_dirs() {
47
48
  PLAYBOOKS_GLOBAL_DIR="$home_dir/.claude/playbooks"
48
49
 
49
50
  local config="$repo_root/harness.config.yml"
50
- [ -r "$config" ] || return 0
51
+ # `-f` as well as `-r`: a FIFO is readable, and awk on it would hang the hook.
52
+ { [ -f "$config" ] && [ -r "$config" ]; } || return 0
51
53
 
52
54
  # One awk pass: within the top-level `paths:` block, emit `<key>\t<value>` for
53
55
  # the (uncommented) `playbooks` / `playbooks_global` entries — inline comments
@@ -70,34 +70,67 @@ variables and the DTCG JSON:
70
70
  - `name` — the variable's dotted path. Map it to/from the tool's own grouping separator.
71
71
  - `value` for a literal; `ref` for an alias (the name of the variable it points at).
72
72
  - `modes` — per-theme overrides (the base theme is `value`/`ref`, not repeated).
73
+ - Every `value` and mode value is a **string** — `"1024"`, not `1024` — and every `ref` a
74
+ non-empty one; each variable carries a `value` or a `ref` (a `value` beside a `ref` is
75
+ ignored), and `modes` is an object whose mode names are not blank nor `__proto__`. A name
76
+ is dot-separated segments, none empty, none starting with `$`, none `__proto__`, not just
77
+ a tier (`primitive`, `semantic`, `component`), and no two variables share one. The CLI
78
+ refuses a file that breaks any of these, naming the variable (and the mode, for a mode
79
+ value).
73
80
 
74
81
  The CLI maps this to the 3-tier DTCG model: `ref` → an alias `{path}`, `modes` →
75
- `$extensions["com.lemony.modes"]`, and the tier follows the path (a literal defaults to
76
- `primitive`, an alias to `semantic` when the name carries no tier).
82
+ `$extensions["com.lemony.modes"]`, and the tier follows the path. A name without a tier
83
+ lands on the one tier `docs/design-tokens.json` already has it in; otherwise a literal
84
+ defaults to `primitive` and an alias to `semantic`. A bare `ref` — or a bare `{alias}` mode
85
+ value — resolves to the tiered path its target lands at in the same neutral file, or to the
86
+ one tier the token file has it in.
77
87
 
78
88
  ## import — tool → JSON
79
89
 
80
90
  1. **Read** the tool's variables over MCP and write them to a neutral file (a temp path).
81
91
  2. **Preview** the diff (writes nothing):
82
92
  `lemony design-tokens import --from=<neutral-file>`. It prints, per token, the target tier
83
- and `new` / `changed` (with the old → new value) / `unchanged`.
93
+ and `new` / `changed` (each thing that changes, old → new: the value, the `$type`, each
94
+ mode) / `unchanged`. When
95
+ `docs/design-tokens.json` already fails validation it adds a `Warning:` saying so: the
96
+ apply will refuse any slice that leaves it failing, so tell the human before curating.
84
97
  3. **Present and curate.** Walk the human through it: which new tokens to take, which changes
85
98
  to accept, and — where a tool-origin name is ambiguous — whether a literal belongs in
86
99
  `primitive` or `semantic`. The human owns the slice.
87
100
  4. **Apply** the agreed slice:
88
101
  `lemony design-tokens import --from=<neutral-file> --apply --only=<dotted,paths>`.
89
102
  This does the additive merge into `docs/design-tokens.json` deterministically (it
90
- bootstraps the file if it does not exist yet). Then run `lemony design-tokens validate` to
91
- confirm the result is well-formed.
103
+ bootstraps the file if it does not exist yet and the slice writes something). A changed
104
+ token keeps what the neutral file does not carry — `$description`, a contrast pairing,
105
+ other extensions — and an unchanged one is left as it is. A token the tool cannot hold
106
+ (see export) is never written over: the preview shows it as a change from
107
+ `(outside the sync: <why>)`, and the apply lists it as not applied. It writes nothing
108
+ when the merged file would fail validation — typically a `semantic` alias whose
109
+ `primitive` target is neither in the file nor in the slice: add the target to the same
110
+ `--only`, or fix the file first. When the neutral file does not carry the target at
111
+ all, the refusal says so: create it in the tool first, or leave out what references
112
+ it. An `--only` path no variable lands at is refused rather than skipped. A refusal
113
+ prints the preview again, so the slice can be re-curated from the paths it lists. Then run
114
+ `lemony design-tokens validate` to confirm the result is well-formed.
92
115
 
93
116
  ## export — JSON → tool
94
117
 
95
- 1. **Plan.** Read the tool's current variables over MCP into a neutral file, then:
118
+ 1. **Plan.** `export` refuses (exit 1) a `docs/design-tokens.json` that fails
119
+ `lemony design-tokens validate` — fix the file first. Read the tool's current variables
120
+ over MCP into a neutral file, then:
96
121
  `lemony design-tokens export --tool-state=<tool-state> --out=<projection-file>`. It prints
97
122
  the additive upsert plan (how many to create, how many to update — **never any deletes**)
98
123
  and writes the projection the tool should hold to `<projection-file>`. The `--tool-state`
99
124
  is optional; without it every variable is planned as a create (the upsert is idempotent by
100
- name either way).
125
+ name either way). A tool variable named without its tier (`color.brand`) is matched to the
126
+ path `import` lands it at (`primitive.color.brand`), so when you push, write into that
127
+ existing variable rather than creating a tiered twin. A token a tool variable cannot hold
128
+ is not projected: a composite (a shadow, a typography, a cubic Bézier — an object or
129
+ array `$value`), a token whose modes the neutral format cannot carry (a mode value that
130
+ is not text, a number or a boolean, a blank or `__proto__` mode name, a modes extension
131
+ that is not an object), and any alias to one of those. The plan names each as "not
132
+ projected", with why — say so to the human. `--out` takes a file path (not a pipe, nor
133
+ the token file itself).
101
134
  2. **Confirm.** Show the plan; the human approves.
102
135
  3. **Push.** Write the projection's variables into the tool over MCP — create new ones, update
103
136
  changed ones, leave tool-only variables untouched.
@@ -88,10 +88,13 @@ rollback:
88
88
  # extensions (.css/.scss/.ts/.tsx/.vue/.svelte/.astro/.js/.mdx/.html/…). Add extra
89
89
  # suffixes here for a stack the built-ins don't cover — additive, never a replacement.
90
90
  # Default none.
91
- # `verify`: a command line `design-tokens contrast` runs after its own WCAG checks
91
+ # `verify`: a command line `design-tokens contrast` runs alongside its own WCAG checks
92
92
  # (through `sh -c`, in the repo root) — your design system's own verifier for the rules
93
93
  # only it can know (palette under colour-vision-deficiency simulation, property grammar,
94
- # CSS scans). Its output is forwarded; a non-zero exit fails the gate. Default none.
94
+ # CSS scans). Its output is forwarded; a non-zero exit fails the gate. It runs on every
95
+ # path: with no docs/design-tokens.json only the WCAG pair check is skipped, and a file
96
+ # that cannot be read or parsed is reported alongside it — never the verifier you
97
+ # declared. Default none.
95
98
  # design_tokens:
96
99
  # scan_extensions:
97
100
  # - .foo