@radicool/throughline 0.12.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/LICENSE +21 -0
- package/README.md +214 -0
- package/adapters/codex/AGENTS.md +29 -0
- package/adapters/codex/codex-mcp.toml +4 -0
- package/adapters/codex/prompts/component-builder.md +272 -0
- package/adapters/codex/prompts/component-pipeline.md +81 -0
- package/adapters/codex/prompts/design-system-audit.md +122 -0
- package/adapters/codex/prompts/design-system-status.md +41 -0
- package/adapters/codex/prompts/figma-environment-setup.md +456 -0
- package/adapters/codex/prompts/icon-system-builder.md +268 -0
- package/adapters/codex/prompts/new-component.md +13 -0
- package/adapters/codex/prompts/repository-builder.md +163 -0
- package/adapters/codex/prompts/retrofit-planner.md +136 -0
- package/adapters/codex/prompts/start.md +22 -0
- package/adapters/codex/prompts/storybook-chromatic-builder.md +261 -0
- package/adapters/codex/prompts/sync-figma-tokens.md +22 -0
- package/adapters/codex/prompts/token-builder.md +361 -0
- package/adapters/codex/prompts/token-crosswalk-builder.md +120 -0
- package/adapters/codex/prompts/token-sheet-builder.md +162 -0
- package/adapters/codex/prompts/token-sync-layer.md +232 -0
- package/adapters/cursor/.cursor/commands/design-system-status.md +41 -0
- package/adapters/cursor/.cursor/commands/new-component.md +13 -0
- package/adapters/cursor/.cursor/commands/start.md +22 -0
- package/adapters/cursor/.cursor/commands/sync-figma-tokens.md +22 -0
- package/adapters/cursor/.cursor/mcp.json +14 -0
- package/adapters/cursor/.cursor/rules/component-builder.mdc +276 -0
- package/adapters/cursor/.cursor/rules/component-pipeline.mdc +85 -0
- package/adapters/cursor/.cursor/rules/design-system-audit.mdc +126 -0
- package/adapters/cursor/.cursor/rules/figma-environment-setup.mdc +460 -0
- package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +272 -0
- package/adapters/cursor/.cursor/rules/repository-builder.mdc +167 -0
- package/adapters/cursor/.cursor/rules/retrofit-planner.mdc +140 -0
- package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +265 -0
- package/adapters/cursor/.cursor/rules/token-builder.mdc +365 -0
- package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +124 -0
- package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +166 -0
- package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +236 -0
- package/adapters/generic/AGENTS.md +46 -0
- package/adapters/generic/commands/design-system-status.md +41 -0
- package/adapters/generic/commands/new-component.md +13 -0
- package/adapters/generic/commands/start.md +22 -0
- package/adapters/generic/commands/sync-figma-tokens.md +22 -0
- package/adapters/generic/skills/component-builder/SKILL.md +272 -0
- package/adapters/generic/skills/component-pipeline/SKILL.md +81 -0
- package/adapters/generic/skills/design-system-audit/SKILL.md +122 -0
- package/adapters/generic/skills/figma-environment-setup/SKILL.md +456 -0
- package/adapters/generic/skills/icon-system-builder/SKILL.md +268 -0
- package/adapters/generic/skills/repository-builder/SKILL.md +163 -0
- package/adapters/generic/skills/retrofit-planner/SKILL.md +136 -0
- package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +261 -0
- package/adapters/generic/skills/token-builder/SKILL.md +361 -0
- package/adapters/generic/skills/token-crosswalk-builder/SKILL.md +120 -0
- package/adapters/generic/skills/token-sheet-builder/SKILL.md +162 -0
- package/adapters/generic/skills/token-sync-layer/SKILL.md +232 -0
- package/package.json +29 -0
- package/references/brainstorm-before-build.md +139 -0
- package/references/brownfield-retrofit.md +93 -0
- package/references/coding-level.md +70 -0
- package/references/crosswalk-schema.md +113 -0
- package/references/figma-component-standards.md +532 -0
- package/references/figma-publishing.md +125 -0
- package/references/figma-scripting.md +278 -0
- package/references/manifest-schema.md +341 -0
- package/references/scaling-up-handoff.md +61 -0
- package/references/sync-adapters.md +141 -0
- package/scripts/README.md +78 -0
- package/scripts/adapters/emit-codex.mjs +58 -0
- package/scripts/adapters/emit-cursor.mjs +26 -0
- package/scripts/adapters/emit-generic.mjs +41 -0
- package/scripts/adapters/generate.mjs +73 -0
- package/scripts/adapters/read-sources.mjs +54 -0
- package/scripts/adapters/translate.mjs +51 -0
- package/scripts/build-reverse-index.mjs +56 -0
- package/scripts/crosswalk.schema.json +33 -0
- package/scripts/grep-color-usage.mjs +143 -0
- package/scripts/guard-token-removal.mjs +95 -0
- package/scripts/install.mjs +143 -0
- package/scripts/lib/crosswalk.mjs +91 -0
- package/scripts/validate-crosswalk.mjs +96 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Scaling up: when a step outgrows a single skill
|
|
2
|
+
|
|
3
|
+
ThroughLine skills are built for **bounded design-system work** — author tokens,
|
|
4
|
+
build a component, scaffold a repo. Sometimes a step inflates into **open-ended
|
|
5
|
+
engineering** with real architectural risk: retrofitting an existing app onto a
|
|
6
|
+
new component library (e.g. → shadcn), migrating a whole motion/interaction layer,
|
|
7
|
+
a multi-package refactor — anything where "build the component" has quietly become
|
|
8
|
+
"re-architect the system." When that happens, **do not just start building.**
|
|
9
|
+
|
|
10
|
+
The valuable behavior here is **recognizing the inflection and planning before
|
|
11
|
+
building** — that behavior is ThroughLine's, and it works whether or not any other
|
|
12
|
+
plugin is installed. Handing off to a planning partner (below) is an *enhancement*
|
|
13
|
+
of that behavior, never a requirement for it.
|
|
14
|
+
|
|
15
|
+
## Recognize the inflection
|
|
16
|
+
|
|
17
|
+
Signs the work has outgrown the skill:
|
|
18
|
+
|
|
19
|
+
- The user wants to **retrofit or migrate an existing codebase**, not start clean.
|
|
20
|
+
- The change touches **many files / packages / systems at once**.
|
|
21
|
+
- There are **real risks** to name — breaking existing behavior, a custom layer to
|
|
22
|
+
preserve (e.g. bespoke cursors, magnetic buttons, a motion system), a framework
|
|
23
|
+
migration, design↔code drift.
|
|
24
|
+
- The honest answer to "can this be done in a few well-understood steps?" is no.
|
|
25
|
+
|
|
26
|
+
## What to do at the inflection (the portable behavior)
|
|
27
|
+
|
|
28
|
+
1. **Stop and surface the shape.** Lay out the **risks** and the **major parts** of
|
|
29
|
+
the work before touching code. Make the scope visible to the user.
|
|
30
|
+
2. **Confirm the scope.** "This is now a real project — do you want to take on all
|
|
31
|
+
of it, or a slice first?" Let the user choose the bite size.
|
|
32
|
+
3. **Switch into structured brainstorm → plan before building.** Big, ambiguous
|
|
33
|
+
work needs a plan, not improvisation.
|
|
34
|
+
|
|
35
|
+
## Where the plan comes from (degrade gracefully — no hard dependency)
|
|
36
|
+
|
|
37
|
+
- **If the Superpowers skills are available** — check the available-skills list for
|
|
38
|
+
`superpowers:brainstorming` and `superpowers:writing-plans` — **hand off to
|
|
39
|
+
them.** They are purpose-built for open-ended engineering: brainstorm the
|
|
40
|
+
approach, then write an executable plan. Announce the handoff so the user
|
|
41
|
+
understands the switch and why.
|
|
42
|
+
- **If Superpowers is not installed** — do **not** stall and do **not** require it.
|
|
43
|
+
Run ThroughLine's own `brainstorm-before-build.md` protocol, scaled to the larger
|
|
44
|
+
scope, and produce a written plan (goals, risks, major parts, ordered steps,
|
|
45
|
+
checkpoints) before building. You may note **once** that Superpowers is a great
|
|
46
|
+
optional partner for this kind of work — then proceed. Never nag, never block.
|
|
47
|
+
|
|
48
|
+
ThroughLine must be able to complete the work **with or without** Superpowers.
|
|
49
|
+
Superpowers makes the big-scope path better; it is never a prerequisite.
|
|
50
|
+
|
|
51
|
+
## The routing rule (keep both directions consistent)
|
|
52
|
+
|
|
53
|
+
This is one rule with two directions — getting one right but not the other is how
|
|
54
|
+
the wrong skill ends up driving:
|
|
55
|
+
|
|
56
|
+
- **Bounded design-system work** (tokens, icons, a single component, repo scaffold)
|
|
57
|
+
→ ThroughLine owns it directly. Do **not** hand a simple setup prompt off to a
|
|
58
|
+
generic brainstorming skill — that is the inverse mistake (see the priority note
|
|
59
|
+
in `figma-environment-setup`).
|
|
60
|
+
- **Open-ended engineering** (retrofits, migrations, app builds) → brainstorm and
|
|
61
|
+
plan first; hand off to Superpowers if present, else plan natively.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Token sync adapters
|
|
2
|
+
|
|
3
|
+
An **adapter** is a coherent preset that turns the normalized DTCG token JSON
|
|
4
|
+
into framework-specific output. Each adapter bundles **four conventions** that
|
|
5
|
+
always travel together — getting them as a unit is what prevents incoherent
|
|
6
|
+
combinations (e.g. "preserve references" + "iOS Swift", which is impossible
|
|
7
|
+
because Swift constants can't do runtime indirection):
|
|
8
|
+
|
|
9
|
+
1. **Where values live** — CSS vars + Tailwind preset, a JS theme object, a
|
|
10
|
+
Swift enum/asset catalog, etc.
|
|
11
|
+
2. **How modes are expressed** — `:root` + `.dark` selectors, MUI palette
|
|
12
|
+
objects, asset-catalog light/dark variants, resource qualifiers.
|
|
13
|
+
3. **Whether semantic→primitive references are preserved or flattened** — web
|
|
14
|
+
adapters preserve (runtime themeable via CSS var indirection); native
|
|
15
|
+
adapters flatten (compile-time constants).
|
|
16
|
+
4. **Naming convention** — `--background` (shadcn), `palette.primary.main`
|
|
17
|
+
(MUI), `Color.backgroundPrimary` (Swift), etc.
|
|
18
|
+
|
|
19
|
+
The DTCG JSON is the **universal input**; the adapter is the **universal output
|
|
20
|
+
contract**. Adding a platform = adding an adapter, never re-authoring Figma.
|
|
21
|
+
|
|
22
|
+
## The two tiers: curated and generated
|
|
23
|
+
|
|
24
|
+
Adapters come in two tiers, and **the skill always tells the user which tier
|
|
25
|
+
they're on** — this manages expectations honestly.
|
|
26
|
+
|
|
27
|
+
### Tier 1 — curated adapters (vetted presets)
|
|
28
|
+
|
|
29
|
+
Five built-in adapters ship with framework-specific knowledge baked in. When the
|
|
30
|
+
user names one of these, use the vetted preset — high confidence, no guessing.
|
|
31
|
+
|
|
32
|
+
| Adapter | Values live in | Modes via | Sem→prim refs | Naming |
|
|
33
|
+
|---|---|---|---|---|
|
|
34
|
+
| `shadcn` | CSS vars + tailwind preset | `:root` + `.dark` | preserved | `--background` |
|
|
35
|
+
| `tailwind` | tailwind theme config | `dark:` variant / class strategy | preserved (via CSS vars) | `colors.background` |
|
|
36
|
+
| `mui` | JS theme object | `createTheme` palettes | preserved (object refs) | `palette.primary.main` |
|
|
37
|
+
| `vanilla-css` | one CSS file | `:root` + `[data-theme]` | preserved | `--color-bg-default` |
|
|
38
|
+
| `ios-swift` | Swift enum / asset catalog | light/dark asset variants | flattened | `Color.backgroundPrimary` |
|
|
39
|
+
|
|
40
|
+
These five were chosen for coverage of this plugin's web-first, design-led
|
|
41
|
+
audience: three React framework adapters (shadcn — the dominant new-project
|
|
42
|
+
choice; standalone Tailwind — for the large Tailwind-without-shadcn population;
|
|
43
|
+
MUI — the enterprise/Material standard), the universal `vanilla-css` escape
|
|
44
|
+
hatch (plain CSS custom properties, no framework), and one native slot
|
|
45
|
+
(`ios-swift`, the more standardized native pattern). Everything else —
|
|
46
|
+
Ant Design, Chakra, HeroUI, Android/Kotlin, Flutter, React Native, etc. — is
|
|
47
|
+
fully supported via Tier 2.
|
|
48
|
+
|
|
49
|
+
`shadcn` vs `tailwind`: shadcn emits CSS vars *plus* the specific var names
|
|
50
|
+
shadcn components expect; `tailwind` targets Tailwind used on its own, mapping
|
|
51
|
+
tokens into the Tailwind theme config. Related but distinct targets.
|
|
52
|
+
|
|
53
|
+
### Tier 2 — generated adapters (any other framework)
|
|
54
|
+
|
|
55
|
+
When the user names a framework **not** in the curated five, the skill does NOT
|
|
56
|
+
refuse and does NOT pretend it's curated. It **generates an adapter** via a
|
|
57
|
+
structured protocol:
|
|
58
|
+
|
|
59
|
+
1. Tell the user plainly this is a generated (not curated) adapter: "Chakra
|
|
60
|
+
isn't one I have a vetted preset for — I'll generate one from its token
|
|
61
|
+
conventions, and we'll verify it against a real component before relying on
|
|
62
|
+
it."
|
|
63
|
+
2. Establish the **four conventions** (the checklist above) for the target
|
|
64
|
+
framework — research its token/theme conventions and confirm with the user
|
|
65
|
+
via the brainstorm-before-build protocol.
|
|
66
|
+
3. Draft the Style Dictionary config + any custom transforms/formats.
|
|
67
|
+
4. **Validate before trusting**: generate output and check it against a sample
|
|
68
|
+
component in that framework — does the framework actually consume these
|
|
69
|
+
tokens cleanly? A generated adapter is unverified until this passes.
|
|
70
|
+
|
|
71
|
+
### Saving validated generated adapters
|
|
72
|
+
|
|
73
|
+
Once a generated adapter is validated, **save it to the repo** at
|
|
74
|
+
`packages/tokens/adapters/<name>/` so future syncs reuse it instead of
|
|
75
|
+
regenerating. Over time the user accumulates their own vetted adapters. Record
|
|
76
|
+
saved custom adapters in the manifest (`sync.customAdapters`). (A validated
|
|
77
|
+
adapter could also be contributed back to the curated set in a future plugin
|
|
78
|
+
version, but that's not required.)
|
|
79
|
+
|
|
80
|
+
## How an adapter is implemented
|
|
81
|
+
|
|
82
|
+
Each adapter is a **Style Dictionary v4 config preset** plus any custom
|
|
83
|
+
transforms/formats it needs. Style Dictionary v4 ingests DTCG natively, so most
|
|
84
|
+
of the work is: register the platform, set the transform group, set the format,
|
|
85
|
+
and (for web adapters) configure `outputReferences: true` so semantic tokens
|
|
86
|
+
emit as references to primitive vars rather than flattened literals.
|
|
87
|
+
|
|
88
|
+
- **Web adapters** (`shadcn`, `tailwind`, `mui`, `vanilla-css`): set
|
|
89
|
+
`outputReferences: true` so `--color-bg-default: var(--color-gray-50)` is
|
|
90
|
+
emitted, preserving the cascade. Modes map to selectors per axis: theme
|
|
91
|
+
overrides (from `Color/Semantic`) under `.dark` / `[data-theme="..."]`, and
|
|
92
|
+
**brand overrides (from the multi-mode `_Color/Primitive`)** under a
|
|
93
|
+
`[data-brand="..."]` selector. Single-mode collections emit flat values in
|
|
94
|
+
`:root` with no theme selector. The
|
|
95
|
+
`shadcn` adapter additionally emits a Tailwind preset mapping the CSS vars to
|
|
96
|
+
Tailwind tokens; the standalone `tailwind` adapter maps tokens into the
|
|
97
|
+
Tailwind theme config directly; `mui` emits a JS theme object whose palette
|
|
98
|
+
references preserve the cascade through object structure.
|
|
99
|
+
- **Native adapters** (`ios-swift`, and generated native targets like
|
|
100
|
+
Android/Kotlin): `outputReferences: false` — references resolve to literal
|
|
101
|
+
values at build time, because the target language has no runtime var
|
|
102
|
+
indirection. Modes map to the platform's native mechanism (asset catalog
|
|
103
|
+
variants, resource qualifiers).
|
|
104
|
+
|
|
105
|
+
## Brownfield value transforms
|
|
106
|
+
|
|
107
|
+
On a retrofit, the values flowing into the adapters get three extra transforms (the
|
|
108
|
+
opacity 0–100→0–1 normalization happens earlier, at extraction). These affect what the
|
|
109
|
+
web adapters emit; native adapters resolve to literals so the channel/`color-mix` forms
|
|
110
|
+
apply to web targets. Full rationale: the 7 guardrails in
|
|
111
|
+
`${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md`.
|
|
112
|
+
|
|
113
|
+
- **Channel alpha (web).** Color tokens emit as space-separated channels
|
|
114
|
+
(`--color-bg-default: 239 68 68;`) consumed via
|
|
115
|
+
`rgb(var(--color-bg-default) / <alpha-value>)`, so Tailwind's `/opacity` modifiers
|
|
116
|
+
keep working. A finished `rgba(...)` would break them.
|
|
117
|
+
- **`/opacity` → `color-mix` (web).** A `/opacity` modifier on a var-based token can't
|
|
118
|
+
fold its alpha into the var; emit `color-mix(in srgb, var(--token) NN%, transparent)`
|
|
119
|
+
instead (or the channel-alpha form).
|
|
120
|
+
- **Float32 rounding at the export boundary.** Round values as they leave the pipeline
|
|
121
|
+
(`Math.round(v*100)/100`) — normalizing inside Figma is a no-op because Figma
|
|
122
|
+
re-quantizes to float32 on store.
|
|
123
|
+
|
|
124
|
+
These are applied in `token-sync-layer`'s extraction/transform step (its "Brownfield
|
|
125
|
+
transforms" subsection), not in the adapter presets.
|
|
126
|
+
|
|
127
|
+
## Output location
|
|
128
|
+
|
|
129
|
+
All adapter output lands in `packages/tokens/` in the monorepo, organized by
|
|
130
|
+
platform (e.g. `packages/tokens/css/`, `packages/tokens/swift/`). These are
|
|
131
|
+
**build artifacts** — never hand-edited, regenerated every sync. The
|
|
132
|
+
`packages/tokens/package.json` exports them for consumers (the UI package,
|
|
133
|
+
Storybook, a future app).
|
|
134
|
+
|
|
135
|
+
## Multiple platforms at once
|
|
136
|
+
|
|
137
|
+
A user can target several platforms (recorded in `sync.platforms`). Generating
|
|
138
|
+
each platform's output is independent and verifiable, which makes it a good fit
|
|
139
|
+
for **parallel subagent generation** — one subagent per adapter, each producing
|
|
140
|
+
and validating its platform's files, reviewed before the combined result is
|
|
141
|
+
landed in a PR. See the token-sync skill for the execution model.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# ThroughLine scripts
|
|
2
|
+
|
|
3
|
+
The executable analog of `references/`: canonical, vetted, **zero-dependency** Node
|
|
4
|
+
(ESM) scripts that the brownfield skills install into a user's monorepo. Authored and
|
|
5
|
+
tested here; copied verbatim by `token-crosswalk-builder` into the user's
|
|
6
|
+
`packages/tokens/scripts/`.
|
|
7
|
+
|
|
8
|
+
| Script | Purpose | Installed as |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| `validate-crosswalk.mjs` | Resolve every `newToken` against the DTCG token source; assert resolved value == `newValue`, N/N. The CI gate. | `tokens:validate` |
|
|
11
|
+
| `build-reverse-index.mjs` | Emit a `codeToken -> newToken` map from the crosswalk to semi-automate SCSS/Tailwind swaps. | `tokens:reverse-index` |
|
|
12
|
+
| `guard-token-removal.mjs` | Grep `.ts/.tsx` (minus generated + tests) for about-to-be-deleted symbols; blocks cleanup until zero references remain. | run during the cleanup phase |
|
|
13
|
+
| `lib/crosswalk.mjs` | Shared loader + structural validation for `crosswalk.json` (used by the validator and reverse-index). | copied alongside |
|
|
14
|
+
| `crosswalk.schema.json` | The finalized JSON Schema for `crosswalk.json` (contract + editor support). | copied beside `crosswalk.json` |
|
|
15
|
+
|
|
16
|
+
The crosswalk contract is documented in
|
|
17
|
+
`${CLAUDE_PLUGIN_ROOT}/references/crosswalk-schema.md`.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node validate-crosswalk.mjs --crosswalk crosswalk.json --tokens dtcg/tokens.json
|
|
23
|
+
node build-reverse-index.mjs --crosswalk crosswalk.json --out crosswalk.reverse.json
|
|
24
|
+
node guard-token-removal.mjs --root . --symbols symbols-to-remove.txt
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Exit codes: `0` success, `1` validation/guard failure (mismatch, missing token,
|
|
28
|
+
conflict, or remaining reference), `2` bad CLI arguments.
|
|
29
|
+
|
|
30
|
+
## How the skill installs these
|
|
31
|
+
|
|
32
|
+
`token-crosswalk-builder` copies `lib/crosswalk.mjs`, `validate-crosswalk.mjs`,
|
|
33
|
+
`build-reverse-index.mjs`, `guard-token-removal.mjs`, and `crosswalk.schema.json`
|
|
34
|
+
into the user's `packages/tokens/scripts/` (schema beside `crosswalk.json`), then
|
|
35
|
+
wires `packages/tokens/package.json`:
|
|
36
|
+
|
|
37
|
+
```jsonc
|
|
38
|
+
"scripts": {
|
|
39
|
+
"tokens:validate": "node scripts/validate-crosswalk.mjs --crosswalk crosswalk.json --tokens dtcg/tokens.json",
|
|
40
|
+
"tokens:reverse-index": "node scripts/build-reverse-index.mjs --crosswalk crosswalk.json --out crosswalk.reverse.json"
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The scripts version with the user's repo so their CI runs them locally — a path
|
|
45
|
+
inside the plugin install would not be reachable from the user's CI.
|
|
46
|
+
|
|
47
|
+
## Tests
|
|
48
|
+
|
|
49
|
+
Run the suite from the repo root (no install step — uses only Node built-ins):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
node --test
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This auto-discovers every `**/*.test.mjs` recursively (31 tests). Don't use
|
|
56
|
+
`node --test scripts/` — a directory positional is treated as a test name on
|
|
57
|
+
Node >=21 and errors; a `scripts/*.test.mjs` glob silently skips `scripts/lib/`.
|
|
58
|
+
|
|
59
|
+
## Multi-agent adapters (`scripts/adapters/`)
|
|
60
|
+
|
|
61
|
+
`SKILL.md`/`commands`/`.mcp.json` are the canonical source. Generated Cursor,
|
|
62
|
+
Codex, and generic-AGENTS.md adapters live in `adapters/<target>/` and are
|
|
63
|
+
committed. After editing any skill or command, regenerate:
|
|
64
|
+
|
|
65
|
+
node scripts/adapters/generate.mjs
|
|
66
|
+
|
|
67
|
+
CI runs `node scripts/adapters/generate.mjs --check` and fails if the committed
|
|
68
|
+
`adapters/` tree is stale. Never hand-edit files under `adapters/` — edit the
|
|
69
|
+
source and regenerate.
|
|
70
|
+
|
|
71
|
+
Users install a target into their own project with the installer, which copies
|
|
72
|
+
the committed `adapters/<target>/` tree plus the runtime payload
|
|
73
|
+
(`references/` + `scripts/`, minus `scripts/adapters/`) into `.throughline/`,
|
|
74
|
+
rewriting `${CLAUDE_PLUGIN_ROOT}` → `.throughline`:
|
|
75
|
+
|
|
76
|
+
npx @radicool/throughline init --target=cursor|codex|generic
|
|
77
|
+
|
|
78
|
+
See `scripts/install.mjs` (pure core + CLI + `install.test.mjs`).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { translateBody, firstSentence, applyPhrasing } from './translate.mjs';
|
|
2
|
+
|
|
3
|
+
const BASE = '.throughline';
|
|
4
|
+
|
|
5
|
+
function tomlValue(v) {
|
|
6
|
+
if (Array.isArray(v)) return `[${v.map((x) => JSON.stringify(x)).join(', ')}]`;
|
|
7
|
+
if (v && typeof v === 'object') {
|
|
8
|
+
return `{ ${Object.entries(v).map(([k, x]) => `${k} = ${JSON.stringify(x)}`).join(', ')} }`;
|
|
9
|
+
}
|
|
10
|
+
return JSON.stringify(v);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function mcpToToml(mcp) {
|
|
14
|
+
const servers = mcp.mcpServers || {};
|
|
15
|
+
const blocks = [];
|
|
16
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
17
|
+
const lines = [`[mcp_servers.${name}]`];
|
|
18
|
+
for (const [k, v] of Object.entries(cfg)) lines.push(`${k} = ${tomlValue(v)}`);
|
|
19
|
+
blocks.push(lines.join('\n'));
|
|
20
|
+
}
|
|
21
|
+
return `${blocks.join('\n\n')}\n`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function indexSection(title, items, note) {
|
|
25
|
+
const lines = [`## ${title}`, ''];
|
|
26
|
+
if (note) { lines.push(note, ''); }
|
|
27
|
+
for (const it of items) {
|
|
28
|
+
lines.push(`- \`${it.name}\` — ${applyPhrasing(firstSentence(it.description), 'codex')} → load \`prompts/${it.name}.md\`.`);
|
|
29
|
+
}
|
|
30
|
+
lines.push('');
|
|
31
|
+
return lines.join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function emitCodex(model) {
|
|
35
|
+
const files = [];
|
|
36
|
+
const agents = [
|
|
37
|
+
'# ThroughLine (Codex adapter)',
|
|
38
|
+
'',
|
|
39
|
+
'ThroughLine builds a design system end to end. Load the matching prompt for the task at hand.',
|
|
40
|
+
'',
|
|
41
|
+
indexSection('ThroughLine skills', model.skills),
|
|
42
|
+
indexSection('ThroughLine commands', model.commands),
|
|
43
|
+
'## MCP servers',
|
|
44
|
+
'',
|
|
45
|
+
'Figma access is provided by the `figma-console` MCP server. See `codex-mcp.toml` for the config to add to your Codex `mcp_servers`.',
|
|
46
|
+
'',
|
|
47
|
+
].join('\n');
|
|
48
|
+
files.push({ path: 'AGENTS.md', content: agents });
|
|
49
|
+
|
|
50
|
+
for (const skill of model.skills) {
|
|
51
|
+
files.push({ path: `prompts/${skill.name}.md`, content: `${translateBody(skill.body, { baseDir: BASE, target: 'codex' })}\n` });
|
|
52
|
+
}
|
|
53
|
+
for (const cmd of model.commands) {
|
|
54
|
+
files.push({ path: `prompts/${cmd.name}.md`, content: `${translateBody(cmd.body, { baseDir: BASE, target: 'codex' })}\n` });
|
|
55
|
+
}
|
|
56
|
+
files.push({ path: 'codex-mcp.toml', content: mcpToToml(model.mcp) });
|
|
57
|
+
return files;
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { translateBody, applyPhrasing } from './translate.mjs';
|
|
2
|
+
|
|
3
|
+
const BASE = '.throughline';
|
|
4
|
+
|
|
5
|
+
function oneLine(s) {
|
|
6
|
+
return s.replace(/\s+/g, ' ').trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function yamlString(s) {
|
|
10
|
+
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function emitCursor(model) {
|
|
14
|
+
const files = [];
|
|
15
|
+
for (const skill of model.skills) {
|
|
16
|
+
const body = translateBody(skill.body, { baseDir: BASE, target: 'cursor' });
|
|
17
|
+
const content = `---\ndescription: ${yamlString(applyPhrasing(oneLine(skill.description), 'cursor'))}\nalwaysApply: false\n---\n${body}\n`;
|
|
18
|
+
files.push({ path: `.cursor/rules/${skill.name}.mdc`, content });
|
|
19
|
+
}
|
|
20
|
+
for (const cmd of model.commands) {
|
|
21
|
+
const body = translateBody(cmd.body, { baseDir: BASE, target: 'cursor' });
|
|
22
|
+
files.push({ path: `.cursor/commands/${cmd.name}.md`, content: `${body}\n` });
|
|
23
|
+
}
|
|
24
|
+
files.push({ path: '.cursor/mcp.json', content: `${JSON.stringify(model.mcp, null, 2)}\n` });
|
|
25
|
+
return files;
|
|
26
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { translateBody, firstSentence, applyPhrasing } from './translate.mjs';
|
|
2
|
+
|
|
3
|
+
const BASE = '.throughline';
|
|
4
|
+
|
|
5
|
+
function indexSection(title, items, ext) {
|
|
6
|
+
const lines = [`## ${title}`, ''];
|
|
7
|
+
for (const it of items) {
|
|
8
|
+
lines.push(`- \`${it.name}\` — ${applyPhrasing(firstSentence(it.description), 'generic')} → read \`${ext(it.name)}\`.`);
|
|
9
|
+
}
|
|
10
|
+
lines.push('');
|
|
11
|
+
return lines.join('\n');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function emitGeneric(model) {
|
|
15
|
+
const files = [];
|
|
16
|
+
const agents = [
|
|
17
|
+
'# ThroughLine (generic AGENTS.md adapter)',
|
|
18
|
+
'',
|
|
19
|
+
'ThroughLine builds a design system end to end. Read the matching skill file for the task at hand.',
|
|
20
|
+
'',
|
|
21
|
+
indexSection('ThroughLine skills', model.skills, (n) => `skills/${n}/SKILL.md`),
|
|
22
|
+
indexSection('ThroughLine commands', model.commands, (n) => `commands/${n}.md`),
|
|
23
|
+
'## MCP servers',
|
|
24
|
+
'',
|
|
25
|
+
'Add the following MCP server to your agent (Figma access):',
|
|
26
|
+
'',
|
|
27
|
+
'```json',
|
|
28
|
+
JSON.stringify(model.mcp, null, 2),
|
|
29
|
+
'```',
|
|
30
|
+
'',
|
|
31
|
+
].join('\n');
|
|
32
|
+
files.push({ path: 'AGENTS.md', content: agents });
|
|
33
|
+
|
|
34
|
+
for (const skill of model.skills) {
|
|
35
|
+
files.push({ path: `skills/${skill.name}/SKILL.md`, content: `${translateBody(skill.body, { baseDir: BASE, target: 'generic' })}\n` });
|
|
36
|
+
}
|
|
37
|
+
for (const cmd of model.commands) {
|
|
38
|
+
files.push({ path: `commands/${cmd.name}.md`, content: `${translateBody(cmd.body, { baseDir: BASE, target: 'generic' })}\n` });
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import { readSources } from './read-sources.mjs';
|
|
5
|
+
import { emitCursor } from './emit-cursor.mjs';
|
|
6
|
+
import { emitCodex } from './emit-codex.mjs';
|
|
7
|
+
import { emitGeneric } from './emit-generic.mjs';
|
|
8
|
+
|
|
9
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
10
|
+
const OUT_ROOT = join(REPO_ROOT, 'adapters');
|
|
11
|
+
|
|
12
|
+
export function generate(model) {
|
|
13
|
+
return { cursor: emitCursor(model), codex: emitCodex(model), generic: emitGeneric(model) };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function writeTargets(outRoot, result) {
|
|
17
|
+
for (const [target, files] of Object.entries(result)) {
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const abs = join(outRoot, target, file.path);
|
|
20
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
21
|
+
writeFileSync(abs, file.content);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function walkFiles(dir) {
|
|
27
|
+
if (!existsSync(dir)) return [];
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
+
if (entry.name === '.DS_Store') continue;
|
|
31
|
+
const abs = join(dir, entry.name);
|
|
32
|
+
if (entry.isDirectory()) out.push(...walkFiles(abs));
|
|
33
|
+
else out.push(abs);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Returns a list of drift descriptions ([] === in sync) comparing result to disk.
|
|
39
|
+
export function diffTargets(outRoot, result) {
|
|
40
|
+
const problems = [];
|
|
41
|
+
const expected = new Set();
|
|
42
|
+
for (const [target, files] of Object.entries(result)) {
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const abs = join(outRoot, target, file.path);
|
|
45
|
+
expected.add(abs);
|
|
46
|
+
if (!existsSync(abs)) { problems.push(`missing: ${target}/${file.path}`); continue; }
|
|
47
|
+
if (readFileSync(abs, 'utf8') !== file.content) problems.push(`changed: ${target}/${file.path}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const target of Object.keys(result)) {
|
|
51
|
+
for (const abs of walkFiles(join(outRoot, target))) {
|
|
52
|
+
if (!expected.has(abs)) problems.push(`orphan: ${abs.slice(outRoot.length + 1)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return problems;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
59
|
+
const result = generate(readSources(REPO_ROOT));
|
|
60
|
+
if (process.argv.includes('--check')) {
|
|
61
|
+
const problems = diffTargets(OUT_ROOT, result);
|
|
62
|
+
if (problems.length) {
|
|
63
|
+
console.error(`✗ adapters out of date (${problems.length}); run: node scripts/adapters/generate.mjs`);
|
|
64
|
+
for (const p of problems) console.error(` ${p}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
console.log('✓ adapters in sync');
|
|
68
|
+
} else {
|
|
69
|
+
writeTargets(OUT_ROOT, result);
|
|
70
|
+
const n = Object.values(result).reduce((a, f) => a + f.length, 0);
|
|
71
|
+
console.log(`✓ wrote ${n} adapter files to adapters/`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export function parseFrontmatter(text) {
|
|
6
|
+
const m = /^---\n([\s\S]*?)\n---\n?/.exec(text);
|
|
7
|
+
if (!m) return { attrs: {}, body: text };
|
|
8
|
+
const attrs = {};
|
|
9
|
+
for (const line of m[1].split('\n')) {
|
|
10
|
+
const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
11
|
+
if (!kv) continue;
|
|
12
|
+
let v = kv[2].trim();
|
|
13
|
+
if (v.length >= 2 && v[0] === '"' && v[v.length - 1] === '"') v = v.slice(1, -1);
|
|
14
|
+
attrs[kv[1]] = v;
|
|
15
|
+
}
|
|
16
|
+
const body = text.slice(m[0].length).replace(/^\n+/, '');
|
|
17
|
+
return { attrs, body };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readJson(path) {
|
|
21
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function readSources(repoRoot) {
|
|
25
|
+
const plugin = readJson(join(repoRoot, '.claude-plugin', 'plugin.json'));
|
|
26
|
+
const mcp = readJson(join(repoRoot, '.mcp.json'));
|
|
27
|
+
|
|
28
|
+
const skillsDir = join(repoRoot, 'skills');
|
|
29
|
+
const skills = readdirSync(skillsDir, { withFileTypes: true })
|
|
30
|
+
.filter((d) => d.isDirectory())
|
|
31
|
+
.map((d) => {
|
|
32
|
+
const { attrs, body } = parseFrontmatter(readFileSync(join(skillsDir, d.name, 'SKILL.md'), 'utf8'));
|
|
33
|
+
return { name: attrs.name || d.name, description: attrs.description || '', body };
|
|
34
|
+
})
|
|
35
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
36
|
+
|
|
37
|
+
const cmdDir = join(repoRoot, 'commands');
|
|
38
|
+
const commands = readdirSync(cmdDir)
|
|
39
|
+
.filter((f) => f.endsWith('.md'))
|
|
40
|
+
.map((f) => {
|
|
41
|
+
const { attrs, body } = parseFrontmatter(readFileSync(join(cmdDir, f), 'utf8'));
|
|
42
|
+
return { name: f.replace(/\.md$/, ''), description: attrs.description || '', body };
|
|
43
|
+
})
|
|
44
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
45
|
+
|
|
46
|
+
return { plugin, mcp, skills, commands };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
50
|
+
|
|
51
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
52
|
+
const model = readSources(REPO_ROOT);
|
|
53
|
+
console.log(`skills: ${model.skills.length}, commands: ${model.commands.length}`);
|
|
54
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function rewritePluginRoot(text, baseDir) {
|
|
2
|
+
return text.split('${CLAUDE_PLUGIN_ROOT}').join(baseDir);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
// Small, reviewable substitution table. Only NAMED cross-skill references and a
|
|
6
|
+
// few Claude-specific phrasings are rewritten; bare prose ("this skill") reads
|
|
7
|
+
// fine on every target and is deliberately left alone.
|
|
8
|
+
export const PHRASING_RULES = [
|
|
9
|
+
{
|
|
10
|
+
// "the `component-builder` skill" -> per-target noun
|
|
11
|
+
pattern: /the `([a-z][a-z-]*)` skill\b/g,
|
|
12
|
+
cursor: 'the `$1` rule',
|
|
13
|
+
codex: 'the `$1` prompt',
|
|
14
|
+
generic: 'the `$1` skill',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
pattern: /\bthe plugin README\b/g,
|
|
18
|
+
cursor: 'the ThroughLine README',
|
|
19
|
+
codex: 'the ThroughLine README',
|
|
20
|
+
generic: 'the ThroughLine README',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
pattern: /\bClaude Code\b/g,
|
|
24
|
+
cursor: 'Cursor',
|
|
25
|
+
codex: 'Codex',
|
|
26
|
+
generic: 'your coding agent',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: /\bClaude\b/g,
|
|
30
|
+
cursor: 'Cursor',
|
|
31
|
+
codex: 'Codex',
|
|
32
|
+
generic: 'the agent',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export function applyPhrasing(text, target) {
|
|
37
|
+
let out = text;
|
|
38
|
+
for (const rule of PHRASING_RULES) {
|
|
39
|
+
out = out.replace(rule.pattern, rule[target]);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function translateBody(text, { baseDir, target }) {
|
|
45
|
+
return applyPhrasing(rewritePluginRoot(text, baseDir), target);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function firstSentence(text) {
|
|
49
|
+
const m = /^(.*?\.)(\s|$)/.exec(text.trim());
|
|
50
|
+
return (m ? m[1] : text).trim();
|
|
51
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Reverse-index generator: codeToken -> newToken, from crosswalk.json.
|
|
2
|
+
// Semi-automates the SCSS/Tailwind swaps. Zero dependencies.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node build-reverse-index.mjs --crosswalk crosswalk.json --out crosswalk.reverse.json
|
|
6
|
+
import { writeFileSync } from 'node:fs';
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { loadCrosswalk } from './lib/crosswalk.mjs';
|
|
10
|
+
|
|
11
|
+
export function buildReverseIndex(crosswalk) {
|
|
12
|
+
const raw = {};
|
|
13
|
+
const conflicts = [];
|
|
14
|
+
for (const row of crosswalk.tokens) {
|
|
15
|
+
for (const code of row.codeTokens) {
|
|
16
|
+
if (code in raw && raw[code] !== row.newToken) {
|
|
17
|
+
conflicts.push({ codeToken: code, tokens: [raw[code], row.newToken] });
|
|
18
|
+
} else {
|
|
19
|
+
raw[code] = row.newToken;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Sort keys longest-first so a literal find-and-replace can't clobber a longer
|
|
24
|
+
// symbol via a shorter substring (e.g. replace "$blue-100" before "$blue").
|
|
25
|
+
const index = {};
|
|
26
|
+
for (const key of Object.keys(raw).sort((a, b) => b.length - a.length || a.localeCompare(b))) {
|
|
27
|
+
index[key] = raw[key];
|
|
28
|
+
}
|
|
29
|
+
return { index, conflicts };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function main() {
|
|
33
|
+
const { values } = parseArgs({
|
|
34
|
+
options: {
|
|
35
|
+
crosswalk: { type: 'string' },
|
|
36
|
+
out: { type: 'string' },
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (!values.crosswalk || !values.out) {
|
|
40
|
+
console.error('usage: build-reverse-index.mjs --crosswalk <crosswalk.json> --out <reverse.json>');
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
const crosswalk = loadCrosswalk(values.crosswalk);
|
|
44
|
+
const { index, conflicts } = buildReverseIndex(crosswalk);
|
|
45
|
+
writeFileSync(values.out, JSON.stringify(index, null, 2) + '\n');
|
|
46
|
+
console.log(`reverse index: ${Object.keys(index).length} code symbol(s) -> ${values.out}`);
|
|
47
|
+
if (conflicts.length) {
|
|
48
|
+
console.error(`\n${conflicts.length} conflict(s) — one code symbol maps to multiple new tokens:`);
|
|
49
|
+
for (const c of conflicts) console.error(` - ${c.codeToken}: ${c.tokens.join(' vs ')}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
55
|
+
main();
|
|
56
|
+
}
|