@catchdrift/cli 0.1.18 → 0.1.19

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.
@@ -0,0 +1,229 @@
1
+ ---
2
+ description: "Sync Figma and/or Storybook into drift.config.ts and regenerate CLAUDE.md, .cursorrules, and .windsurfrules. Run after adding new DS components or updating Figma."
3
+ allowed-tools: Read, Glob, Grep, Bash, Edit, Write
4
+ argument-hint: "[figma | storybook | tokens | status | --dry-run]"
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # /drift-sync — Sync Figma + Storybook → update manifest
9
+
10
+ Pull the latest component state from Figma and/or Storybook and keep
11
+ `drift.config.ts`, `CLAUDE.md`, `.cursorrules`, and `.windsurfrules` in sync.
12
+ Works with any product team — property management, SaaS, fintech, consumer, B2B.
13
+
14
+ ## Arguments: `$ARGUMENTS`
15
+ - *(no args)* — sync from both Storybook and Figma, update all config files
16
+ - `figma` — pull component list from Figma only (all pages)
17
+ - `storybook` — re-discover components from Storybook index only
18
+ - `tokens` — run figma-sync (design tokens + icons only, no component changes)
19
+ - `status` — report sync status without making changes (safe read-only audit)
20
+ - `--dry-run` — show what would change without writing any files
21
+
22
+ ---
23
+
24
+ ## Step 1 — Read current state
25
+
26
+ Read `drift.config.ts` (or `src/ds-coverage/config.ts`) to get:
27
+ - **Figma source** — the config may use either shape:
28
+ - Single file: `figmaFileKey` + optional `figmaComponentPages` (one-file setup)
29
+ - Multi-file: `figmaFiles: [{ key: string, componentPages?: string[] }, ...]` (components spread across multiple Figma files, e.g. Core DS, Icons, Patterns)
30
+ - Normalise both into a working array: `const files = config.figmaFiles ?? (config.figmaFileKey ? [{ key: config.figmaFileKey, componentPages: config.figmaComponentPages }] : [])`
31
+ - `storybookUrl`
32
+ - `components` — the current registry (component name → storyPath / figmaLink)
33
+ - `threshold`
34
+ - Any `approvedGaps` entries
35
+
36
+ **Component pages** are an inclusive filter — only pull components from these pages. If `componentPages` is not set for a file, include all pages (and flag any page whose name contains "wip", "in progress", "draft", "proposal", "graveyard", or "archive" as potentially unready so the team can review them).
37
+
38
+ ---
39
+
40
+ ## Step 2 — Dispatch
41
+
42
+ ### `status` → Read-only audit
43
+
44
+ Report sync status across all sources without changing anything:
45
+
46
+ ```
47
+ ## Drift Sync Status — <date>
48
+
49
+ Storybook: <storybookUrl>
50
+ Last successful sync: <date if known, else "unknown">
51
+ Reachable: ✅ / ❌
52
+
53
+ Figma files: <N> configured
54
+ figma.com/design/<key1> Reachable: ✅ / ❌
55
+ figma.com/design/<key2> Reachable: ✅ / ❌ (if multi-file)
56
+ FIGMA_API_TOKEN: ✅ set / ❌ missing
57
+
58
+ Config: drift.config.ts
59
+ Components registered: N
60
+ Missing storyPath: N
61
+ Missing figmaLink: N
62
+ Approved gaps: N
63
+
64
+ AI rules files:
65
+ CLAUDE.md: ✅ / ❌ (stale — last component: <X>)
66
+ .cursorrules: ✅ / ❌
67
+ .windsurfrules: ✅ / ❌
68
+ ```
69
+
70
+ ---
71
+
72
+ ### `tokens` or no args → sync tokens from Figma
73
+
74
+ Run:
75
+ ```bash
76
+ npm run figma-sync
77
+ ```
78
+
79
+ Requires `FIGMA_API_TOKEN`. If missing:
80
+ ```
81
+ export FIGMA_API_TOKEN=your-token
82
+ npm run figma-sync
83
+
84
+ Get token: figma.com → Profile → Settings → Security → Personal access tokens
85
+ ```
86
+
87
+ After running, report which token categories updated (colors added/changed, typography changes, spacing changes).
88
+
89
+ ---
90
+
91
+ ### `storybook` or no args → re-discover from Storybook
92
+
93
+ Fetch `{storybookUrl}/index.json`. The response has a flat map of all stories
94
+ across all story files. Parse it to get unique component names and story paths.
95
+
96
+ If Storybook isn't reachable, try the deployed URL (`chromaticUrl`) from config. If
97
+ neither is reachable, report:
98
+ ```
99
+ ⚠️ Storybook not reachable at <url>
100
+ Run `npm run storybook` first, or provide your deployed Storybook URL in drift.config.ts:
101
+ chromaticUrl: 'https://main--abc123.chromatic.com'
102
+ ```
103
+
104
+ Compare against `config.components` and report:
105
+
106
+ ```
107
+ ## Storybook sync
108
+
109
+ New (in Storybook, not in config):
110
+ + DataTable → story: data-table--default
111
+ + FilterBar → story: filters-filter-bar--default
112
+
113
+ Removed (in config, no matching story):
114
+ - OldWidget
115
+
116
+ Unchanged: 34 components
117
+ ```
118
+
119
+ Ask for each new/removed component before changing config. For removed components,
120
+ ask whether to delete from config or keep with a `deprecated: true` flag.
121
+
122
+ ---
123
+
124
+ ### `figma` or no args → pull all published components from Figma
125
+
126
+ **Key:** Figma components live on different pages. Use the dedicated
127
+ `/components` endpoint — it returns ALL published components across ALL pages
128
+ with page metadata included. Do NOT try to walk the file tree page by page.
129
+
130
+ Iterate over every file in the normalised `files` array (see Step 1). For each:
131
+ ```
132
+ GET https://api.figma.com/v1/files/{file.key}/components
133
+ Headers: X-Figma-Token: {FIGMA_API_TOKEN}
134
+ ```
135
+
136
+ Each component in the response has:
137
+ - `name` — full path e.g. `"Button/Primary/Default"` or `"Forms/Input/Filled"`
138
+ - `node_id` — unique ID for building the figmaLink URL
139
+ - `containing_frame.name` — the frame it lives in
140
+ - `containing_frame.pageName` — **the Figma page it's on**
141
+ - `description` — designer's notes (preserve this — use as component description in config)
142
+
143
+ Group and display results by file, then by page within each file. If multiple files are configured, prefix each section with the file key/URL so it's clear which file the components come from:
144
+ ```
145
+ ## Figma components found
146
+
147
+ ### figma.com/design/<key1> (Core DS)
148
+ 📄 Primitives (12 components)
149
+ ✅ Button → in config
150
+ ✅ Input → in config
151
+ ❌ Toggle → NOT in config (node: 123:456)
152
+ ❌ Checkbox → NOT in config (node: 123:789)
153
+
154
+ 📄 🚧 In Progress (3 components)
155
+ ⚠️ SearchBar → draft (will not be added)
156
+ ⚠️ FilterChip → draft (will not be added)
157
+
158
+ ### figma.com/design/<key2> (Icons & Patterns)
159
+ 📄 Patterns (8 components)
160
+ ✅ TenantsTable → in config
161
+ ❌ DataGrid → NOT in config (node: 456:123)
162
+ ```
163
+
164
+ For each file, apply the `componentPages` filter:
165
+ - If `componentPages` is set: only include components from those pages; skip everything else
166
+ - If not set: include all pages, but flag any page matching "wip/draft/graveyard/archive/proposal" in a separate "Unreviewed pages" section so the team can decide whether to add them
167
+
168
+ For each component NOT in config (excluding draft pages), ask:
169
+ ```
170
+ Add these to drift.config.ts?
171
+ - Toggle (key1 / Primitives page)
172
+ - Checkbox (key1 / Primitives page)
173
+ - DataGrid (key2 / Patterns page)
174
+
175
+ For each, I'll also add the figmaLink pointing to that node.
176
+ Reply with which ones to add, or "all" / "none".
177
+ ```
178
+
179
+ When adding, build the figmaLink URL using the file key that component came from:
180
+ ```
181
+ https://www.figma.com/design/{file.key}?node-id={node_id}
182
+ ```
183
+
184
+ Also report components in the codebase (from static scan of `src/`) that exist
185
+ in Storybook but have NO matching Figma component — these are code-first gaps:
186
+ ```
187
+ In code but not in Figma (consider pushing to Figma):
188
+ ⚡ OccupancyWidget — used 1× — run /drift-push OccupancyWidget to add
189
+ ⚡ FMKPIRow — used 1×
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Step 3 — Update files
195
+
196
+ After confirmation (or immediately if `--dry-run` was NOT passed), update:
197
+
198
+ 1. **`drift.config.ts`** — add components with storyPath + figmaLink where available; mark removed with `deprecated: true` rather than deleting (preserves history)
199
+ 2. **`CLAUDE.md`** — regenerate only the components table (preserve all other content — rules, workflow, etc.)
200
+ 3. **`.cursorrules`** — same regeneration (Cursor reads this automatically)
201
+ 4. **`.windsurfrules`** — same regeneration (Windsurf reads this automatically); create if it doesn't exist
202
+
203
+ For AI rules files, regenerate ONLY the component table section, bounded by these markers:
204
+ ```
205
+ <!-- drift:components-start -->
206
+ ...
207
+ <!-- drift:components-end -->
208
+ ```
209
+ If markers don't exist, append the table at the end of the file.
210
+
211
+ Report:
212
+ ```
213
+ ## Sync complete — <date>
214
+
215
+ drift.config.ts — +3 added, 0 deprecated (37 total)
216
+ CLAUDE.md — components table updated (37 components)
217
+ .cursorrules — components table updated
218
+ .windsurfrules — created (37 components)
219
+
220
+ New figmaLinks added: Toggle, Checkbox, DataGrid
221
+ New storyPaths added: DataTable, FilterBar
222
+
223
+ Storybook links: 37/37 ✅
224
+ Figma links: 31/37 (6 missing — run /drift-push gaps to add them)
225
+
226
+ Next: npm run dev and press D to see the updated overlay.
227
+ ```
228
+
229
+ If `--dry-run` was passed, show what would change without writing anything.
@@ -0,0 +1,252 @@
1
+ ---
2
+ description: "Analyze DS coverage across the codebase. Find gaps, suggest DS replacements, migrate components, approve exceptions, promote candidates. Sub-commands: fix, approve, promote, manifest, check, history, audit."
3
+ allowed-tools: Read, Glob, Grep, Bash, Edit
4
+ argument-hint: "[fix <ComponentName> | approve <Name> \"<reason>\" | promote <Name> | manifest | check | history | audit]"
5
+ ---
6
+
7
+ # /drift — Design System Drift Analyzer
8
+
9
+ Analyze design system coverage for this codebase. Identify components that drift from
10
+ the DS, suggest replacements, and optionally migrate code. Works with any React
11
+ product team — property management, SaaS, fintech, e-commerce, etc.
12
+
13
+ ## Arguments: `$ARGUMENTS`
14
+
15
+ Supported sub-commands:
16
+ - *(no args)* — full coverage report + top gap analysis
17
+ - `fix <ComponentName>` — migrate one custom component to its DS equivalent
18
+ - `approve <ComponentName> "<rationale>"` — approve a gap with documented rationale
19
+ - `promote <ComponentName>` — flag a high-frequency custom component for DS promotion
20
+ - `manifest` — print the DS component registry with story + Figma links
21
+ - `check` — run the headless drift-check script and parse results
22
+ - `history` — show coverage trend over last N scans (from saved reports)
23
+ - `audit` — full audit mode: coverage + token violations + rationale gaps + promotion candidates
24
+
25
+ ---
26
+
27
+ ## Step 1 — Read the DS registry
28
+
29
+ Read `src/ds-coverage/config.ts` (or `drift.config.ts` at project root) to understand:
30
+ - Every registered DS component, its story path, and Figma link
31
+ - `threshold` — the CI pass/fail threshold
32
+ - `storybookUrl` and `figmaFileKey`
33
+
34
+ ---
35
+
36
+ ## Step 2 — Dispatch on $ARGUMENTS
37
+
38
+ ### No arguments → Full Coverage Report
39
+
40
+ 1. Glob `src/**/*.tsx` (excluding `src/stories/`, `src/tokens/`, `node_modules/`, `*.stories.*`, `*.test.*`, `*.spec.*`) to find all screens, views, and feature files.
41
+ 2. Grep each file for JSX component usage. Classify every component as:
42
+ - **DS** — name is in `config.components`
43
+ - **Approved gap** — custom, but has an approval entry (check for `// drift-approved:` comment or approval record)
44
+ - **Custom** — name is not in `config.components` and not approved
45
+ 3. Compute per-file and overall DS coverage %.
46
+ 4. List the top custom components by frequency (the gap map).
47
+ 5. For any custom component used ≥ 3 times:
48
+ - Check whether a DS equivalent exists → suggest it
49
+ - If used ≥ 5 times → flag as **promotion candidate**
50
+ 6. Print a report in this format:
51
+
52
+ ```
53
+ ## Drift Report — <date>
54
+
55
+ Overall DS coverage: XX% (threshold: XX%)
56
+ Status: ✅ PASS or 🔴 FAIL
57
+
58
+ ### By file
59
+ | File | DS | Custom | Approved | Coverage |
60
+ |------|----|--------|----------|----------|
61
+ | ... | .. | ... | ... | ...% |
62
+
63
+ ### Top gaps (custom components not in DS)
64
+ | Component | Uses | Status | Suggested DS replacement |
65
+ |-----------|------|--------|--------------------------|
66
+ | BtnGrp | 6 | ⚠️ Gap | Use `<Tabs>` (segmented variant) |
67
+ | LinkBtn | 5 | 🔁 Promote candidate | Use `<Button variant="ghost">` |
68
+ | AvatarRow | 3 | ✅ Approved — "needed for nav micro-interaction" | — |
69
+
70
+ ### Token violations
71
+ | File | Violation | Line |
72
+ |------|-----------|------|
73
+ | ... | Hardcoded `#3b82f6` — use `var(--ds-color-brand-500)` | 42 |
74
+
75
+ ### Promotion candidates (used ≥5× with no DS equivalent)
76
+ These components appear frequently enough to justify adding to the DS:
77
+ 1. <ComponentName> — used N× across N files
78
+ → Run `/drift promote <ComponentName>` to create a promotion request
79
+
80
+ ### Recommendations
81
+ 1. Highest impact: migrate <X> → saves N% coverage across N files
82
+ 2. ...
83
+ ```
84
+
85
+ ---
86
+
87
+ ### `fix <ComponentName>`
88
+
89
+ 1. Find all usages of `<ComponentName>` across `src/` (excluding stories and tests).
90
+ 2. Read the DS equivalent component file to understand its props API.
91
+ 3. For each usage, generate a code diff that replaces the custom component with the DS component, preserving existing behavior.
92
+ 4. Show a summary first:
93
+ ```
94
+ Found 6 usages of <ComponentName> across 3 files.
95
+ DS equivalent: <DSName> — props mapping:
96
+ old.size="large" → new.size="lg"
97
+ old.color="red" → new.variant="danger"
98
+
99
+ Estimated coverage improvement: +2.3%
100
+
101
+ Apply changes? (yes/no/preview)
102
+ ```
103
+ 5. After applying, re-run coverage calculation and show before/after.
104
+
105
+ ---
106
+
107
+ ### `approve <ComponentName> "<rationale>"`
108
+
109
+ Approve a custom component as an intentional exception to DS coverage rules.
110
+
111
+ Use this when a custom component is genuinely needed and cannot be replaced by a DS component. Rationale must include:
112
+ - Why no DS component covers this need
113
+ - Whether it should be proposed for DS inclusion in the future
114
+
115
+ 1. Verify the component is actually used in the codebase.
116
+ 2. Check it's not already approved.
117
+ 3. Add an approval entry to `drift.config.ts`:
118
+ ```ts
119
+ approvedGaps: {
120
+ '<ComponentName>': {
121
+ rationale: '<rationale>',
122
+ approvedBy: '<ask for name>',
123
+ approvedAt: '<today ISO date>',
124
+ promoteToDS: true/false, // ask: "Should this be proposed for DS inclusion?"
125
+ }
126
+ }
127
+ ```
128
+ 4. Also add an inline comment convention to the usage site so rationale travels with the code:
129
+ ```tsx
130
+ {/* drift:ignore reason="<rationale>" approvedBy="<name>" */}
131
+ <ComponentName ... />
132
+ ```
133
+ This is visible in code review without needing to cross-reference config.
134
+ 5. Confirm: "Approved. This component will show as ✅ Approved in drift reports and will not count against coverage."
135
+
136
+ ---
137
+
138
+ ### `promote <ComponentName>`
139
+
140
+ Flag a high-frequency custom component as a DS promotion candidate.
141
+
142
+ 1. Read the component file (or search for it if it's a one-off inline component).
143
+ 2. Count its usage frequency and list the files it appears in.
144
+ 3. Generate a promotion brief:
145
+ ```
146
+ ## DS Promotion Request: <ComponentName>
147
+
148
+ **Usage:** N× across N files
149
+ **Files:** list up to 5 most common locations
150
+
151
+ **Props API (current):**
152
+ <extracted interface or inferred from usage>
153
+
154
+ **Suggested DS entry:**
155
+ <ComponentName>: {
156
+ storyPath: '<suggested-story-path>',
157
+ }
158
+
159
+ **Design request:** This component appears frequently enough to warrant a
160
+ Figma design + DS review. Recommend filing a design request.
161
+
162
+ Next steps:
163
+ 1. Designer creates the spec in Figma
164
+ 2. Run /drift-push <ComponentName> to attach implementation notes
165
+ 3. After Figma review, run /drift-sync to register it officially
166
+ ```
167
+ 4. Ask if user wants to open a Jira ticket (if `jiraBaseUrl` is configured).
168
+
169
+ ---
170
+
171
+ ### `manifest`
172
+
173
+ Print a formatted table of all DS components from `config.components`:
174
+
175
+ ```
176
+ ## DS Component Registry — <date>
177
+
178
+ | Component | Story | Figma | Status |
179
+ |--------------------|-------|-------|--------|
180
+ | Button | ✅ | ✅ | Stable |
181
+ | Tabs | ✅ | — | Needs Figma |
182
+ | ... | ... | ... | ... |
183
+
184
+ Approved gaps (N):
185
+ | Component | Rationale | Approved by |
186
+ |--------------|-----------|-------------|
187
+ | CustomHeader | "..." | Michelle, 2026-01-15 |
188
+
189
+ Missing story paths: X components
190
+ Missing Figma links: X components
191
+ Run /drift-sync to fill gaps automatically.
192
+ ```
193
+
194
+ ---
195
+
196
+ ### `check`
197
+
198
+ Run the headless drift check and parse results:
199
+
200
+ ```bash
201
+ npm run build && npx vite preview --port 4173 &
202
+ npx wait-on http://localhost:4173 --timeout 30000
203
+ node scripts/drift-check.mjs --url http://localhost:4173 --json > /tmp/drift-report.json
204
+ ```
205
+
206
+ Then read `/tmp/drift-report.json` and print the full report format including:
207
+ - Coverage % vs threshold
208
+ - Per-route breakdown
209
+ - Token violations (hardcoded colors, spacing)
210
+ - Gap map with promotion candidates
211
+
212
+ ---
213
+
214
+ ### `history`
215
+
216
+ Read any saved `drift-report-*.json` files or GitHub Actions artifacts from `.github/`.
217
+ Show coverage trend:
218
+
219
+ ```
220
+ ## Coverage History
221
+
222
+ Date Coverage Delta Status
223
+ 2026-03-30 78% +2% 🔴 Below threshold
224
+ 2026-03-23 76% +1% 🔴 Below threshold
225
+ 2026-03-16 75% — 🔴 Below threshold
226
+
227
+ Trend: ↑ improving (+3% over 3 weeks)
228
+ At current rate, threshold (80%) reached in ~2 weeks.
229
+ ```
230
+
231
+ ---
232
+
233
+ ### `audit`
234
+
235
+ Full audit combining all modes:
236
+ 1. Run `check` (headless scan)
237
+ 2. Run the static analysis (no-args path)
238
+ 3. Cross-reference: flag any component approved as a gap that now has a DS equivalent
239
+ 4. List components that have been custom for ≥ 30 days (from git log if available)
240
+ 5. Output a single comprehensive report suitable for a DS quarterly review
241
+
242
+ ---
243
+
244
+ ## Style rules for output
245
+
246
+ - Lead with the numbers — coverage %, counts, file names
247
+ - Use exact component names from the codebase (case-sensitive)
248
+ - When suggesting a DS replacement, always show a before/after code snippet
249
+ - If coverage is below threshold, surface the 3 highest-impact gaps first with estimated improvement per fix
250
+ - Never suggest creating new components — only DS components from `config.components`
251
+ - For teams unfamiliar with the DS, always explain *why* a replacement is better, not just *what* to use
252
+ - Approved gaps always show ✅ and are excluded from failure calculations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catchdrift/cli",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "CLI for Drift — install, check, and manage design system coverage for any React app.",
5
5
  "keywords": [
6
6
  "design-system",
@@ -31,6 +31,7 @@
31
31
  "bin",
32
32
  "src",
33
33
  "scripts",
34
+ "commands",
34
35
  "LICENSE",
35
36
  "README.md"
36
37
  ],
@@ -32,6 +32,7 @@ import {
32
32
  import {
33
33
  writeDriftConfig,
34
34
  writeAIRulesFiles,
35
+ writeClaudeSkills,
35
36
  patchAppEntry,
36
37
  writeGithubAction,
37
38
  } from '../lib/writers.mjs'
@@ -349,7 +350,8 @@ export async function init(argv) {
349
350
  storybookUrl: storybookUrl || '',
350
351
  figmaFiles: figmaFiles.length ? figmaFiles : undefined,
351
352
  })
352
- spinner.stop(`Written: ${rulesFiles.join(', ')}`)
353
+ const skillFiles = writeClaudeSkills(cwd)
354
+ spinner.stop(`Written: ${rulesFiles.join(', ')}${skillFiles.length ? ` + ${skillFiles.length} Claude skills` : ''}`)
353
355
 
354
356
  // ── Step 8: Install @catchdrift/overlay ──────────────────────────────────────
355
357
  const pkgJson = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'))
@@ -417,6 +419,7 @@ export async function init(argv) {
417
419
  ${pc.bold('What was created:')}
418
420
  drift.config.ts ${pc.dim('DS component registry')}
419
421
  ${rulesFiles.map(f => f.padEnd(34)).join('\n ')}${pc.dim('AI constraints')}
422
+ ${skillFiles.length ? `.claude/commands/ ${pc.dim('Claude Code skills (/drift-sync, /drift-scaffold, etc.)')}` : ''}
420
423
  ${addCI ? '.github/workflows/drift-check.yml ' + pc.dim('CI drift check on every PR') : ''}
421
424
  ${patched ? patched.padEnd(34) + pc.dim('DriftOverlay added (dev-only)') : ''}
422
425
 
@@ -3,11 +3,14 @@
3
3
  * All writers are idempotent — safe to re-run.
4
4
  */
5
5
 
6
- import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs'
6
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync } from 'fs'
7
7
  import { resolve, join, dirname, relative, posix } from 'path'
8
+ import { fileURLToPath } from 'url'
8
9
  import { buildComponentRegistry } from './storybook.mjs'
9
10
  import { findAppEntry } from './detect.mjs'
10
11
 
12
+ const __dirname = dirname(fileURLToPath(import.meta.url))
13
+
11
14
  // ── drift.config.ts ───────────────────────────────────────────────────────────
12
15
 
13
16
  export function writeDriftConfig(cwd, { storybookUrl, chromaticUrl, figmaFiles, dsPackages, threshold, components }) {
@@ -327,3 +330,26 @@ jobs:
327
330
 
328
331
  writeFileSync(filepath, yaml, 'utf8')
329
332
  }
333
+
334
+ // ── Claude Code skill files ───────────────────────────────────────────────────
335
+
336
+ export function writeClaudeSkills(cwd) {
337
+ // Skills ship with the CLI package under commands/
338
+ const skillsSource = resolve(__dirname, '../../commands')
339
+ if (!existsSync(skillsSource)) return []
340
+
341
+ const destDir = join(cwd, '.claude', 'commands')
342
+ mkdirSync(destDir, { recursive: true })
343
+
344
+ const written = []
345
+ for (const file of readdirSync(skillsSource)) {
346
+ if (!file.endsWith('.md')) continue
347
+ const dest = join(destDir, file)
348
+ // Don't overwrite files the user may have customised
349
+ if (!existsSync(dest)) {
350
+ writeFileSync(dest, readFileSync(join(skillsSource, file), 'utf8'), 'utf8')
351
+ written.push(file)
352
+ }
353
+ }
354
+ return written
355
+ }