grails-cli 0.0.0-stage → 0.4.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/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "grails-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "description": "Grails Code Sync from the command line: reads a component library's code and uploads its design tokens and component specs to Grails.",
5
+ "license": "UNLICENSED",
6
+ "homepage": "https://docs.grails.design/cli/overview/",
7
+ "keywords": [
8
+ "grails",
9
+ "design-system",
10
+ "design-tokens",
11
+ "dtcg",
12
+ "components",
13
+ "figma",
14
+ "react",
15
+ "tailwind",
16
+ "cli",
17
+ "code-sync"
18
+ ],
19
+ "packageManager": "pnpm@10.32.1",
20
+ "type": "module",
21
+ "bin": {
22
+ "grails-cli": "./dist/cli.js"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "templates"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "registry": "https://registry.npmjs.org/"
31
+ },
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "scripts": {
36
+ "build": "node scripts/build.mjs",
37
+ "smoke": "node scripts/smoke.mjs",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit"
39
+ },
40
+ "dependencies": {
41
+ "typescript": "5.8.3"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.0.0",
45
+ "esbuild": "^0.27.7"
46
+ }
47
+ }
@@ -0,0 +1,56 @@
1
+ # Grails Code Sync: on every push to the branch your team syncs from (a merge into it), read this repository's
2
+ # design tokens and components and upload them to Grails, so the vault shows what the code actually holds.
3
+ #
4
+ # SETUP
5
+ # 1. In Grails, create an API key with the write:code scope: Team Settings > Integrations > API Keys.
6
+ # 2. Save it as a repository secret named GRAILS_API_KEY: Settings > Secrets and variables > Actions.
7
+ # 3. Say in grails.code.json which branch syncs ("branch": "main", or "preview", …): `npx grails-cli init`
8
+ # proposes it. Put the SAME branch under `branches:` below.
9
+ # 4. Commit this file as .github/workflows/grails-cli-sync.yml.
10
+ #
11
+ # THE KEY stays in the step's `env:`. It is never written into a `run:` line, never echoed, and never passed as
12
+ # an argument: the CLI reads it from the environment and refuses it as a flag. Do not change that, and do not run
13
+ # this on `pull_request`, where a fork's run has no secrets and no business syncing.
14
+ #
15
+ # WHAT LEAVES THE REPOSITORY: design tokens (names, types, values), component names, file paths, and each
16
+ # component's spec (props, anatomy names, resolved style values). Never source code. `npx grails-cli tokens
17
+ # --json` prints the exact token list. `sync --dry-run` stores nothing: with the key it asks Grails what would
18
+ # change, without it it counts locally.
19
+ #
20
+ # EXIT CODES: 0 done · 1 the sync ended incomplete · 2 misconfigured (grails.code.json declares no branch, too) · 3
21
+ # refused by the server (a CLI older than the minimum, too) · 4 the server could not be reached or failed. Only 4 is
22
+ # worth retrying.
23
+ #
24
+ # THE BRANCH: the CLI uploads only from the branch grails.code.json declares. Run anywhere else it uploads nothing,
25
+ # exits 0 so the job stays green, and says so: a notice on the run, the job summary, and the step output
26
+ # `steps.grails.outputs.result` = `skipped-branch` (otherwise `synced`, `preview`, `offline` or `failed`).
27
+ #
28
+ # THE VERSION: `npx --yes grails-cli` fetches the latest release from npm on every run. To choose when a new release
29
+ # reaches your pushes, pin a version (`grails-cli@<version>`) so a new release cannot change a push you did not look at.
30
+ name: Grails Code Sync
31
+
32
+ on:
33
+ push:
34
+ # The branch grails.code.json declares. Keep the two equal.
35
+ branches: [main]
36
+
37
+ permissions:
38
+ contents: read
39
+
40
+ jobs:
41
+ sync:
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ - uses: actions/setup-node@v4
46
+ with:
47
+ node-version: 22
48
+ # Needed when the library uses Tailwind (its tokens are read with the project's OWN tailwindcss; without the
49
+ # dependencies installed the run exits 2), and better in any case: with them the scanner reads the types the
50
+ # way the project does (the "typed" run). Use your package manager's command.
51
+ # - run: npm ci
52
+ - name: Sync the design tokens and the components to Grails
53
+ id: grails
54
+ env:
55
+ GRAILS_API_KEY: ${{ secrets.GRAILS_API_KEY }}
56
+ run: npx --yes grails-cli sync --trigger ci
@@ -0,0 +1,337 @@
1
+ ---
2
+ name: grails-cli
3
+ description: Use when working with the Grails CLI (`grails-cli`) in a component library's repository. That covers setting up `grails.code.json`, syncing components and design tokens to Grails from a terminal or from CI, writing DTCG token files that Grails accepts, starting a first DTCG version from the scanner's tokens, listing the token changes people asked for in Grails, and fixing a sync that failed. Triggers on grails-cli, grails.code.json, Grails Code Sync, GRAILS_API_KEY, DTCG upload, design tokens to Grails, tokens pending, tokens baseline.
4
+ ---
5
+
6
+ # The Grails CLI
7
+
8
+ Grails (grails.design) keeps a design system's Figma files and its code side by side. This CLI reads your
9
+ component library's repository (its components and its design tokens) and syncs them to a Grails project.
10
+
11
+ **It never changes your code.** It writes only the files you explicitly ask it to: `init --write` writes
12
+ `grails.code.json`, `scan --out <file>` writes the manifest, and `tokens baseline --out <dir>` writes the scanner's
13
+ tokens as DTCG files (both refuse a path inside the repository unless you pass `--in-repo`). When people in Grails ask
14
+ for a token change, the CLI lists the change, and you or your build make it in your own source.
15
+
16
+ ## Install
17
+
18
+ `npm i -D grails-cli` in the repository (Node 20 or newer), or run any command without installing it as
19
+ `npx grails-cli <command>`. The commands below are written `grails-cli`. The GitHub workflow template the package
20
+ ships (`templates/grails-cli-sync.yml`) runs `npx --yes grails-cli sync`: pin a version in it (`grails-cli@<version>`)
21
+ to choose when a new release reaches your pushes.
22
+
23
+ ## Commands
24
+
25
+ | command | what it does | sends anything? |
26
+ |---|---|---|
27
+ | `grails-cli init [root] [--write]` | proposes a `grails.code.json` from the code, with the evidence for each decision | no |
28
+ | `grails-cli scan [root] [--out <file>]` | writes the component manifest Grails would receive | no |
29
+ | `grails-cli tokens [root] [--json]` | the design tokens a sync would upload (with `dtcg`: the files, or the exact upload with `--json`) | no |
30
+ | `grails-cli tokens baseline [root] --out <dir> [--in-repo]` | writes the scanner's tokens as DTCG files and a resolver, a first version to own | no |
31
+ | `grails-cli sync [root] [--dry-run] [--json]` | uploads the tokens, then the components | yes (with `--dry-run`: asks what would change, stores nothing) |
32
+ | `grails-cli tokens pending [root] [--collection <name>] [--request <id>] [--json]` | the token changes people asked for in Grails: type, before → after, and the id; the filters narrow it | reads only |
33
+ | `grails-cli tokens discard <change-id> \| --all [--collection <name>] \| --request <id> --reason "<why>" [--dry-run]` | closes one change, or a set in one step, that your team will not carry, with the reason | yes (Grails keeps the changes, closed); `--dry-run` changes nothing |
34
+ | `grails-cli rules [root] [--set code=figma]` | the naming rules that match code tokens to Figma ones | edits need `write:tokens` |
35
+ | `grails-cli connect [--prompt]` | keeps the API key in the operating system's keychain (checked against Grails first) so no command needs `GRAILS_API_KEY`; reads the clipboard and clears it, or with `--prompt` asks in a hidden prompt | reads only (one authenticated read to validate the key) |
36
+ | `grails-cli disconnect` | removes the key `connect` stored (the key itself is not revoked) | no |
37
+ | `grails-cli update [root]` | asks the npm registry now for the latest `grails-cli` and prints the command that updates it for the project's package manager; it does not run it | asks the npm registry only |
38
+ | `grails-cli status` | the project's code sources, their last syncs, and how many changes asked for in Grails wait for each | reads only |
39
+ | `grails-cli chi [root]` | the Component Health Index of every component | no |
40
+ | `grails-cli check-classes [root]` | Tailwind classes that are probably misspelt | no |
41
+ | `grails-cli skill` | prints this file | no |
42
+
43
+ **The API key** is read from `GRAILS_API_KEY` in the environment (CI, and anyone who prefers it) and, when that is not
44
+ set, from the operating system's keychain, where `grails-cli connect` put it; and from nowhere else. Never pass it as a
45
+ flag or an argument, and never print the environment. To create one, go to Team Settings › Integrations › API Keys in
46
+ Grails and give it the `write:code` scope. `GRAILS_API_URL` points the CLI at another API; the keychain keeps one key
47
+ per API, by its host.
48
+
49
+ **An agent does not run `connect` for the person.** `connect` takes the key from the person's clipboard or from a hidden
50
+ prompt on their terminal; a key an agent was handed in a chat is already in a transcript, so it does not go to
51
+ `connect --prompt` either. When there is no key, tell the person to run `grails-cli connect` in their own terminal (or to set
52
+ `GRAILS_API_KEY`), and carry on with the commands that need none (`init`, `scan`, `tokens`, `sync --dry-run`).
53
+
54
+ **Updates.** Once a day the CLI asks the npm registry for the latest version and, while it is behind, every run ends with
55
+ one line on stderr naming the two versions and the update command. That line is not an error and does not change the
56
+ exit code. `GRAILS_CLI_NO_UPDATE_CHECK=1` turns it off. `grails-cli update` prints the command and never runs it: update
57
+ the version only when the person asks.
58
+
59
+ ## `grails.code.json`
60
+
61
+ ```json
62
+ {
63
+ "branch": "main",
64
+ "components": ["src/components/**/*.tsx"],
65
+ "exclude": ["**/*.stories.tsx"],
66
+ "dtcg": { "files": ["tokens/**/*.json"], "resolver": "tokens/resolver.json" }
67
+ }
68
+ ```
69
+
70
+ - **`branch`** is required for a real sync. It names the branch that IS this source in Grails: the one merges land
71
+ on. Run anywhere else, the sync uploads nothing and exits 0.
72
+ - **`components`** and **`exclude`** are globs, relative to the root.
73
+ - **`dtcg`** names your DTCG files. **When it is set, it is the only token source**: the scanner reads no token from
74
+ Tailwind or CSS, and `css` and `tokens` are ignored with a warning.
75
+ - `files` are globs of `.json` files.
76
+ - `resolver` is optional, and is a DTCG resolver document. Its `$ref`s name the files relative to its own folder,
77
+ so every file must sit in or below that folder.
78
+ - `cssVariables` is optional: how your CSS names the tokens, so Grails can tell which token a `var(--x)` is. It holds
79
+ the `prefix` of your build, the `exceptions` to the naming, and `ignore`, patterns for custom properties that are
80
+ not your tokens. See "Telling Grails which token a `var(--x)` is".
81
+ - **`css`** (CSS files whose custom properties are tokens) and **`tokens`** (TypeScript token objects) are for the
82
+ scanner, when there is no `dtcg`.
83
+
84
+ ## Tokens: two ways in
85
+
86
+ 1. **DTCG files (the recommended way).** Generate them with your tool of choice: a Style Dictionary build,
87
+ Tokens Studio's DTCG export, Specify, your own script. Declare them in `dtcg`, and `sync` uploads them as they are.
88
+ Grails reads them and answers with precise findings when it cannot.
89
+ 2. **The scanner (a convenience).** With no `dtcg`, the CLI reads a Tailwind 3 or 4 theme, the CSS custom properties
90
+ of the files in `css`, and the token objects in `tokens`. It is good for a first sync. Anything it does not
91
+ support should be exported to DTCG rather than worked around. To move to DTCG, run `tokens baseline --out <dir>`,
92
+ review the files, commit them and declare them in `dtcg`. From then on the files are yours and the scanner stops
93
+ being the source. The command lists the values the scanner left out (they are not in the files), refuses to run while
94
+ `dtcg` is set, and never writes into a folder that has files.
95
+
96
+ With DTCG, a sync records which components each component renders, and which tokens it uses **only through a `var(--x)`
97
+ that `cssVariables` ties to a token** (next section). Every `var(--x)` it cannot tie is listed, so you can fix it.
98
+
99
+ ## Telling Grails which token a `var(--x)` is
100
+
101
+ *This section is written for a coding agent that is asked to set it up. Follow the steps in order, and never guess a
102
+ name.*
103
+
104
+ Your components write `var(--color-brand-500)`; your DTCG files say `color.brand.500`. With `dtcg` declared, the scanner
105
+ reads no token, so the only thing that ties the two is how your build names a token's custom property. Grails assumes
106
+ **Style Dictionary's default**, the `css` transform group (`name/kebab`):
107
+
108
+ - the token's path segments, joined with `-`, lower case, behind two dashes: `color.brand.500` is `--color-brand-500`;
109
+ - with the build's `prefix` first: `prefix: "ds"` makes it `--ds-color-brand-500`;
110
+ - camelCase is split (`fontSize` → `font-size`, `h1Bold` → `h1-bold`), but **a digit stays with its letters** (`2xl`
111
+ stays `2xl`, `body2` stays `body2`);
112
+ - anything that is not a letter or a digit is a separator (`Brand Primary`, `brand_primary` and `brand-primary` all give
113
+ `brand-primary`);
114
+ - the name is **case-sensitive**: `var(--Color-Brand-500)` is no token, because the build writes lower case.
115
+
116
+ Where a name does not follow that, you write an **exception**: the custom property and the token it is. A custom
117
+ property that is **not your token at all** (a library's, the framework's) goes in **`ignore`**, as a pattern.
118
+
119
+ ```json
120
+ {
121
+ "dtcg": {
122
+ "files": ["tokens/**/*.json"],
123
+ "resolver": "tokens/resolver.json",
124
+ "cssVariables": {
125
+ "prefix": "ds",
126
+ "exceptions": { "--brand": "color.brand.500", "--surface": "color.semantic.surface" },
127
+ "ignore": ["--radix-*", "--tw-*"]
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ All three keys are optional. An exception's value is the token's dotted path in the files (the first segment is its
134
+ collection). **An exception wins over the convention**, and one that names a path the files do not have is reported.
135
+
136
+ **`ignore` is a list of patterns**: a name as `var()` writes it, two dashes first, with `*` for the part that varies
137
+ (`*` is the only wildcard, and at most 200 patterns). A name an `ignore` pattern covers is not resolved, not counted,
138
+ not listed and never sent, **even when the convention would have found a token of that name**; an exception for the same
139
+ name wins over `ignore`. A pattern that covers nothing the components use is not an error: `--json` lists it in
140
+ `css_variables.ignore_unused`, and the terminal line says how many were ignored (`· 14 ignored`), never which.
141
+
142
+ **The steps:**
143
+
144
+ 1. **Look at how the repository builds its CSS from the DTCG files.** Find the Style Dictionary configuration
145
+ (`config.json`, `style-dictionary.config.*`, `sd.config.*`, or a script that calls it) and read, for the CSS platform,
146
+ its `prefix`, its `transformGroup` and its `transforms`. Then open the CSS it generates and look at a few real names:
147
+ a name in the output beats a reading of the configuration.
148
+ - `transformGroup: "css"` (or `name/kebab` in `transforms`), and no custom `name` transform: the convention applies.
149
+ - `name/camel`, `name/snake`, `name/constant`, `name/pascal` or a custom `name` transform: the names do not follow
150
+ the convention, and each one a component uses needs an exception.
151
+ 2. **Set the prefix**: `"cssVariables": { "prefix": "<the build's prefix>" }`, and leave it out when the build has none.
152
+ 3. **Run `grails-cli sync --dry-run`.** It needs no key and sends nothing (with a key it also asks Grails what
153
+ would change). `--json` adds a `css_variables` object with the same list. The line `css variables` counts the custom
154
+ properties your components use and how many resolve to a token.
155
+ 4. **Read the unresolved list** (the `!` lines under it), and decide each one:
156
+ - `var(--x) is no token of the DTCG files`: find where `--x` comes from, and choose by who owns it.
157
+ - **It is your token under another name**: generated from a DTCG token, or an alias of a generated one
158
+ (`--x: var(--ds-color-brand-500)`). Add it to **`exceptions`** with that token's path. An exception says what a
159
+ name IS.
160
+ - **It is not your token, and never will be**: a library's, the framework's, one the page sets at run time. Add a
161
+ pattern that covers the family to **`ignore`**, so the list holds only what a person still has to decide.
162
+ Examples that are common: Radix sets sizes for its popovers and menus
163
+ (`--radix-dropdown-menu-content-available-height`, `--radix-select-trigger-width`: pattern `--radix-*`);
164
+ Tailwind's own internals, which a component reads when it writes a ring or a shadow by hand
165
+ (`--tw-ring-color`, `--tw-ring-offset-color`: pattern `--tw-*`); a value the page sets from JavaScript (`--vh`,
166
+ `--scroll-y`). A custom property one component declares for itself (`--card-padding`) is also not a token: ignore
167
+ it, by name or by a prefix that component's team already uses.
168
+ - **You cannot tell which of the two it is**: do not ignore it and do not guess an exception. Leave it listed and
169
+ ask the person.
170
+ - `var(--x) is the name of N tokens`: two tokens spell the same CSS name (`brandPrimary` and `brand-primary`).
171
+ Read the generated CSS to see which one the build keeps (the last declaration wins), and add that one to
172
+ `exceptions`. Better, rename one of them in the DTCG files.
173
+ - `dtcg.cssVariables.exceptions: --x names color.y, which is not a token of the DTCG files`: the exception is
174
+ mistyped, or the token was renamed or removed. Fix its path (`grails-cli tokens --json` prints the files) or
175
+ delete the exception.
176
+ - `whose name has a "/"`: a level of that token's name contains a `/`, which a usage upload cannot spell. Rename the
177
+ level in the DTCG files.
178
+ 5. **Run step 3 again** until every remaining `!` line is one you decided to leave for the person. Then tell them what
179
+ you added to `exceptions` and to `ignore` and what you left, with the reason for each.
180
+ 6. **Prune** with `grails-cli sync --dry-run --json`: `css_variables.ignore_unused` lists the patterns that ignored
181
+ nothing (no component uses a name they cover, or an exception already names every such name). Delete those.
182
+
183
+ **Keep `ignore` narrow.** A pattern beats the naming convention, so `--*-bg` would hide a real token that happens to end
184
+ in `-bg`: the token would no longer be tied to the components that use it, and the line would not say so. Cover a family
185
+ by its owner's prefix (`--radix-*`), not by a shape (`--*-width`). If a name the pattern covers IS a token, add an
186
+ exception for it: that is the way to keep one inside a broad pattern.
187
+
188
+ **What this reads, and what it does not.** The scanner reads a `var(--x)` a component can use in three places:
189
+
190
+ - **the `style` of any element** in the component, every property (`margin`, `border`, `boxShadow`, …) and not only
191
+ colour, padding and size; also the props libraries use for styles (`contentContainerStyle`, `css`, `sx`), a constant or
192
+ style sheet the style refers to, and every branch of a condition or lookup;
193
+ - **the stylesheet the component's file imports** (`.css`, CSS modules, `.scss`, `.less`), for every component that file
194
+ holds. A custom property a rule *declares* in `:root`, a theme selector or `@theme` is a definition, not a use, so a
195
+ tokens sheet imported by a layout does not make the layout "use" every token. `@import` inside a stylesheet is not
196
+ followed;
197
+ - **Tailwind classes** in `className` and in `cn`, `clsx`, `classnames`, `cx`, `twMerge` and `cva` calls, variants
198
+ included: `bg-[var(--x)]` in either version, `bg-(--x)` in Tailwind 4, `bg-[--x]` in Tailwind 3. (Tailwind 4
199
+ compiles `bg-[--x]` to a literal `--x`, not a `var()`, so it is not read there.)
200
+
201
+ It does **not** read a name a template assembles at run time (`` `var(--space-${n})` ``), a class helper it does not know
202
+ (`tv()`), CSS-in-JS template literals (styled-components, Emotion's `css` tag), a component a file defines but does not
203
+ export, or a property set from JavaScript. So a custom property missing from the list may still be used. A reference
204
+ that is not a `var()` (a Tailwind theme class such as `bg-primary`, a TypeScript token object) is counted in a line ("N
205
+ token reference(s)… are not linked") but not tied to a token. A run never fails for any of this.
206
+
207
+ **Do not** edit the token files or the CSS to make a line disappear: an exception, or a pattern in `ignore`, is the
208
+ team's statement of what a name is, or that it is not theirs.
209
+
210
+ ## Writing DTCG that Grails accepts
211
+
212
+ Grails reads the **DTCG format 2025.10** (designtokens.org) and validates every document against its schema.
213
+
214
+ - **A token's path is its dotted name, and the first segment is its collection.** `color.brand.500` belongs to the
215
+ collection `color`. A token directly at the root (one segment) is refused.
216
+ - **Types.** All 13 DTCG types are accepted: `color` (`{ "colorSpace", "components" }`), `dimension`
217
+ (`{ "value", "unit" }`), `number`, `fontFamily`, `fontWeight`, `duration`, `cubicBezier`, `strokeStyle`, `border`,
218
+ `transition`, `shadow` (one or a list, `inset` allowed), `gradient` and `typography`. A group's `$type` applies to
219
+ the tokens inside it.
220
+ - **Aliases** are `{path}` references, at most 64 deep, with no cycles. A JSON Pointer `$ref` inside a value is
221
+ refused: use `{path}`.
222
+ - **Modes.**
223
+ - With no resolver, each collection has one mode, `default`.
224
+ - With a resolver, its sets give every mode the same value, and a modifier's contexts become the collection's modes.
225
+ - Two or more modifiers give one mode per combination, named with `·` (`light·brand-a`), at most 50.
226
+ - A resolver's `$ref` names only a file of the same upload (or `#/sets/x`, `#/modifiers/x`). Nothing is fetched.
227
+ - **Limits.**
228
+ - At most 500 files and 4 MiB in one upload.
229
+ - At most 20,000 values, 500 collections, 20 modifiers, 50 contexts per modifier, and groups 32 levels deep.
230
+ - The CLI checks the file count and the size before sending anything.
231
+ - **Refused outright:**
232
+ - Tokens Studio's legacy format (`$themes`, `$metadata`, or `value` / `type` without `$`): export as DTCG (W3C).
233
+ - A `boolean` or object `$value` with no `$type`.
234
+
235
+ **What DTCG cannot express.** The token stays valid DTCG, with a `$value` any tool can read, and the extra goes in
236
+ `$extensions` under a `design.grails.*` key:
237
+
238
+ - **Typography.** Use `design.grails.typography.extraProps`: `textCase`, `textDecoration`, `fontStyle`,
239
+ `paragraphSpacing`, `paragraphIndent`, `listSpacing`, `leadingTrim`.
240
+ - **Booleans, copy strings, blurs and layout grids.** Use no `$type`, a readable string `$value` (`"true"`,
241
+ `"blur(4px)"`, `"12 columns, 24px gutter"`), and the real value in
242
+ `design.grails.value: { "type": "boolean" | "string" | "blur" | "grid", "value": … }`.
243
+ - **A colour built from a colour and an opacity token.** Keep the applied colour in `$value`, and name the parts in
244
+ `design.grails.composed: { "color": "{path}" or a DTCG colour, "opacity": "{path}" }`. The opacity token is read
245
+ as it is: a Figma opacity is 0–100, so divide by 100.
246
+
247
+ **What Grails does with them.**
248
+ - It reads a token with `design.grails.value` as that type and that value. The extension wins over `$value`, so a
249
+ `$value` like `"{brand}"` there is text, not a reference.
250
+ - It validates the forms above. A malformed one (a boolean whose value is `"yes"`, a `textCase` of `"shout"`) refuses
251
+ the sync with `extension_invalid` and the pointer. A `{path}` inside them that names no token is `alias_unknown`.
252
+ - What it does not know (a key a form does not have, a value type it has never seen) is kept as written, with the
253
+ warning `extension_not_interpreted`, so a document a newer tool wrote loses nothing.
254
+
255
+ When a sync is refused, each finding names the file, the JSON pointer inside it and a code (`schema_invalid`,
256
+ `token_at_root`, `legacy_format`, `alias_unknown`, `alias_cycle`, `extension_invalid`, …). Fix the source and sync
257
+ again.
258
+
259
+ ## Changes asked for in Grails
260
+
261
+ Admins and editors can ask for token changes in Grails: a new value, a new token, collection or mode, or a removal.
262
+ **Grails never writes your repository, and neither does the CLI.** Closing the loop takes three steps, and the last one
263
+ is automatic:
264
+
265
+ 1. `grails-cli tokens pending` lists the changes by collection: each with its **type**, `before → after` as DTCG, and
266
+ its **id**. The `before` is the value your repository holds now, resolved (an alias in your code shows as the colour
267
+ it resolves to). `--json` has every value exact.
268
+ 2. Carry each change into your token source, or into the rules that generate it, and merge it as any other change.
269
+ 3. The next `grails-cli sync` (in CI, on the declared branch) **marks the change applied** once your repository holds
270
+ its value, and prints how many. Grails compares the resolved value of the token in that mode, with the tolerance the
271
+ Diff uses for colours and sizes, so an alias that resolves to the asked value counts. A token you removed closes
272
+ its change; a created token, collection or mode closes when it exists.
273
+
274
+ To see what the next sync would do **before** it runs, use `grails-cli sync --dry-run` (with the key): it prints how many
275
+ changes it would mark applied, close because their token is gone, or leave open, with their ids, and stores nothing.
276
+ `grails-cli status` shows how many changes wait for each source.
277
+
278
+ **If the repository holds another value, the change is not closed.** Grails keeps proposing its value, checks again on
279
+ every sync, and records what your code holds: running `tokens pending` again after a sync shows the real "before".
280
+
281
+ **A change your team will not carry is closed with a reason, not ignored:**
282
+
283
+ ```sh
284
+ grails-cli tokens discard <change-id> --reason "Handled in the design-tokens repo"
285
+ ```
286
+
287
+ - The id is the one `tokens pending` prints beside the change. Never invent one.
288
+ - `--reason` is required: 1 to 1000 characters on one line. Say why, in words a designer reads: Grails keeps it with the
289
+ change, with who discarded it (the owner of the API key) and that it came from code.
290
+ - Discarding a collection takes its modes and tokens with it. One value of a token being created cannot be discarded
291
+ alone: discard the token.
292
+ - Exit 3 means Grails has no open change with that id for this source, or it was already applied or discarded. Do not
293
+ retry; list again with `tokens pending`.
294
+ - Do it only when the team decided not to carry the change. If it only has not been carried yet, leave it: the sync
295
+ closes it when it arrives.
296
+
297
+ **Many at once.** Exactly one of three picks the changes (the reason is required, as above):
298
+
299
+ ```sh
300
+ grails-cli tokens discard --all --dry-run # look first: nothing is changed
301
+ grails-cli tokens discard --all --reason "Handled in the tokens repo" # everything waiting for this repository
302
+ grails-cli tokens discard --all --collection color --reason "…" # one collection's
303
+ grails-cli tokens discard --request <request-id> --reason "…" # one change request's
304
+ ```
305
+
306
+ - `--dry-run` lists the changes that would be discarded (id, token, before → after) and changes nothing. Run it first
307
+ and read the list: there is no `--yes`, and a discard cannot be undone from the CLI.
308
+ - The set is closed all together or not at all, and at most 1000 at once. Past that the CLI says so and discards
309
+ nothing: narrow it with `--collection` or `--request`.
310
+ - `tokens pending --collection <name>` and `--request <id>` list exactly what the same words would discard. The request
311
+ id is printed under the heading of `tokens pending`.
312
+ - `--collection` goes with `--all` only. Naming a change id and `--all` together is refused.
313
+ - The answer says how many were discarded; `--json` has the ids. Exit 3 means a request or source Grails does not hold
314
+ (nothing was discarded) or a set over the cap.
315
+
316
+ ## In CI
317
+
318
+ The package ships `templates/grails-cli-sync.yml`, a GitHub Actions workflow that runs `npx --yes grails-cli sync --trigger ci`
319
+ on a push to the declared branch.
320
+
321
+ - Store the key as the repository secret `GRAILS_API_KEY`. It reaches the step only through `env:`, never inside a
322
+ `run:` line.
323
+ - Do not run it on `pull_request`.
324
+ - The run writes a job summary, and the step output `result` is `synced`, `preview`, `offline`, `skipped-branch` or
325
+ `failed`.
326
+
327
+ **Exit codes:**
328
+
329
+ | code | meaning |
330
+ |---|---|
331
+ | 0 | done (synced, unchanged, or a dry run) |
332
+ | 1 | the sync ended incomplete |
333
+ | 2 | misconfigured: a flag, `grails.code.json` (no `branch`, a DTCG file that is not JSON or matches nothing), or the key |
334
+ | 3 | refused by the server: the key, the plan, or the content (DTCG findings included) |
335
+ | 4 | the server could not be reached or failed |
336
+
337
+ Only 4 is worth retrying.