@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/adapters/codex/AGENTS.md +29 -0
  4. package/adapters/codex/codex-mcp.toml +4 -0
  5. package/adapters/codex/prompts/component-builder.md +272 -0
  6. package/adapters/codex/prompts/component-pipeline.md +81 -0
  7. package/adapters/codex/prompts/design-system-audit.md +122 -0
  8. package/adapters/codex/prompts/design-system-status.md +41 -0
  9. package/adapters/codex/prompts/figma-environment-setup.md +456 -0
  10. package/adapters/codex/prompts/icon-system-builder.md +268 -0
  11. package/adapters/codex/prompts/new-component.md +13 -0
  12. package/adapters/codex/prompts/repository-builder.md +163 -0
  13. package/adapters/codex/prompts/retrofit-planner.md +136 -0
  14. package/adapters/codex/prompts/start.md +22 -0
  15. package/adapters/codex/prompts/storybook-chromatic-builder.md +261 -0
  16. package/adapters/codex/prompts/sync-figma-tokens.md +22 -0
  17. package/adapters/codex/prompts/token-builder.md +361 -0
  18. package/adapters/codex/prompts/token-crosswalk-builder.md +120 -0
  19. package/adapters/codex/prompts/token-sheet-builder.md +162 -0
  20. package/adapters/codex/prompts/token-sync-layer.md +232 -0
  21. package/adapters/cursor/.cursor/commands/design-system-status.md +41 -0
  22. package/adapters/cursor/.cursor/commands/new-component.md +13 -0
  23. package/adapters/cursor/.cursor/commands/start.md +22 -0
  24. package/adapters/cursor/.cursor/commands/sync-figma-tokens.md +22 -0
  25. package/adapters/cursor/.cursor/mcp.json +14 -0
  26. package/adapters/cursor/.cursor/rules/component-builder.mdc +276 -0
  27. package/adapters/cursor/.cursor/rules/component-pipeline.mdc +85 -0
  28. package/adapters/cursor/.cursor/rules/design-system-audit.mdc +126 -0
  29. package/adapters/cursor/.cursor/rules/figma-environment-setup.mdc +460 -0
  30. package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +272 -0
  31. package/adapters/cursor/.cursor/rules/repository-builder.mdc +167 -0
  32. package/adapters/cursor/.cursor/rules/retrofit-planner.mdc +140 -0
  33. package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +265 -0
  34. package/adapters/cursor/.cursor/rules/token-builder.mdc +365 -0
  35. package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +124 -0
  36. package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +166 -0
  37. package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +236 -0
  38. package/adapters/generic/AGENTS.md +46 -0
  39. package/adapters/generic/commands/design-system-status.md +41 -0
  40. package/adapters/generic/commands/new-component.md +13 -0
  41. package/adapters/generic/commands/start.md +22 -0
  42. package/adapters/generic/commands/sync-figma-tokens.md +22 -0
  43. package/adapters/generic/skills/component-builder/SKILL.md +272 -0
  44. package/adapters/generic/skills/component-pipeline/SKILL.md +81 -0
  45. package/adapters/generic/skills/design-system-audit/SKILL.md +122 -0
  46. package/adapters/generic/skills/figma-environment-setup/SKILL.md +456 -0
  47. package/adapters/generic/skills/icon-system-builder/SKILL.md +268 -0
  48. package/adapters/generic/skills/repository-builder/SKILL.md +163 -0
  49. package/adapters/generic/skills/retrofit-planner/SKILL.md +136 -0
  50. package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +261 -0
  51. package/adapters/generic/skills/token-builder/SKILL.md +361 -0
  52. package/adapters/generic/skills/token-crosswalk-builder/SKILL.md +120 -0
  53. package/adapters/generic/skills/token-sheet-builder/SKILL.md +162 -0
  54. package/adapters/generic/skills/token-sync-layer/SKILL.md +232 -0
  55. package/package.json +29 -0
  56. package/references/brainstorm-before-build.md +139 -0
  57. package/references/brownfield-retrofit.md +93 -0
  58. package/references/coding-level.md +70 -0
  59. package/references/crosswalk-schema.md +113 -0
  60. package/references/figma-component-standards.md +532 -0
  61. package/references/figma-publishing.md +125 -0
  62. package/references/figma-scripting.md +278 -0
  63. package/references/manifest-schema.md +341 -0
  64. package/references/scaling-up-handoff.md +61 -0
  65. package/references/sync-adapters.md +141 -0
  66. package/scripts/README.md +78 -0
  67. package/scripts/adapters/emit-codex.mjs +58 -0
  68. package/scripts/adapters/emit-cursor.mjs +26 -0
  69. package/scripts/adapters/emit-generic.mjs +41 -0
  70. package/scripts/adapters/generate.mjs +73 -0
  71. package/scripts/adapters/read-sources.mjs +54 -0
  72. package/scripts/adapters/translate.mjs +51 -0
  73. package/scripts/build-reverse-index.mjs +56 -0
  74. package/scripts/crosswalk.schema.json +33 -0
  75. package/scripts/grep-color-usage.mjs +143 -0
  76. package/scripts/guard-token-removal.mjs +95 -0
  77. package/scripts/install.mjs +143 -0
  78. package/scripts/lib/crosswalk.mjs +91 -0
  79. 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
+ }