@rokkit/cli 1.1.12 → 1.1.14

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/README.md CHANGED
@@ -135,3 +135,21 @@ lib/
135
135
  ---
136
136
 
137
137
  Part of [Rokkit](https://github.com/jerrythomas/rokkit) — a Svelte 5 component library and design system.
138
+
139
+ ## AI skills
140
+
141
+ Rokkit ships curated AI skills (SKILL.md guides) that teach coding agents how to
142
+ use the library well — theming with the named-token system and building UI with
143
+ the components.
144
+
145
+ ```bash
146
+ rokkit skills list # see the catalog
147
+ rokkit skills add semantic-styles-rokkit # install one
148
+ rokkit skills add # interactive multi-select
149
+ rokkit skills add --all # install everything
150
+ rokkit skills add <name> --force # overwrite an existing install
151
+ ```
152
+
153
+ Skills install into your project's `.claude/skills/<name>/`, so they're shared
154
+ with your team via version control. Browse the source under
155
+ [`packages/cli/skills/`](./skills).
@@ -77,12 +77,20 @@ export function generateUnoConfig(): string;
77
77
  export function generateAppCssImports(themes: string[]): string[];
78
78
  /**
79
79
  * Generate the flash-prevention script for app.html.
80
- * Returns null when no script is needed (system switcher).
80
+ *
81
+ * Themes flip on `[data-mode="dark"]` only — there is no `@media
82
+ * (prefers-color-scheme)` fallback — so every switcher needs a script that
83
+ * resolves the mode pre-paint (mirrors `@rokkit/unocss`'s `themeHook`):
84
+ * - `system` tracks the OS preference directly (no persisted toggle).
85
+ * - `manual` / `full` read the persisted mode and resolve a stored-or-default
86
+ * `'system'`/`'auto'` against the OS preference (default: follow the OS).
87
+ *
81
88
  * @param {string} switcher — 'system' | 'manual' | 'full'
82
89
  * @param {string} [storageKey='rokkit-theme']
83
- * @returns {string | null}
90
+ * @param {string} [defaultStyle='rokkit']
91
+ * @returns {string} the `<script>` tag to inject after `<body>`
84
92
  */
85
- export function generateInitScript(switcher: string, storageKey?: string, defaultStyle?: string): string | null;
93
+ export function generateInitScript(switcher: string, storageKey?: string, defaultStyle?: string): string;
86
94
  /**
87
95
  * Interactive init command — prompts the user, writes config files.
88
96
  * @param {Record<string, unknown>} [_opts]
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Parse `name` and `description` from a SKILL.md YAML frontmatter block.
3
+ * @param {string} md
4
+ * @returns {{ name: string, description: string }}
5
+ */
6
+ export function parseFrontmatter(md: string): {
7
+ name: string;
8
+ description: string;
9
+ };
10
+ /**
11
+ * Read the bundled skill catalog (derived from each SKILL.md's frontmatter).
12
+ * @param {{ skillsDir?: string }} [opts]
13
+ * @returns {Array<{ name: string, description: string }>}
14
+ */
15
+ export function listSkills({ skillsDir }?: {
16
+ skillsDir?: string;
17
+ }): Array<{
18
+ name: string;
19
+ description: string;
20
+ }>;
21
+ /**
22
+ * Copy named skills into <cwd>/.claude/skills/<name>/ (idempotent).
23
+ * @param {string[]} names
24
+ * @param {{ cwd?: string, force?: boolean, skillsDir?: string }} [opts]
25
+ * @returns {Array<{ name: string, status: 'added' | 'skipped' | 'unknown' }>}
26
+ */
27
+ export function installSkills(names: string[], { cwd, force, skillsDir }?: {
28
+ cwd?: string;
29
+ force?: boolean;
30
+ skillsDir?: string;
31
+ }): Array<{
32
+ name: string;
33
+ status: "added" | "skipped" | "unknown";
34
+ }>;
35
+ /**
36
+ * Print the catalog, marking already-installed skills.
37
+ * @param {{ skillsDir?: string, cwd?: string }} [opts]
38
+ */
39
+ export function runSkillsList({ skillsDir, cwd }?: {
40
+ skillsDir?: string;
41
+ cwd?: string;
42
+ }): Promise<void>;
43
+ /**
44
+ * Resolve which skills to install, then copy and report.
45
+ * @param {string[]} names
46
+ * @param {{ all?: boolean, force?: boolean, skillsDir?: string, cwd?: string }} [opts]
47
+ */
48
+ export function runSkillsAdd(names: string[], { all, force, skillsDir, cwd }?: {
49
+ all?: boolean;
50
+ force?: boolean;
51
+ skillsDir?: string;
52
+ cwd?: string;
53
+ }): Promise<void>;
54
+ /**
55
+ * CLI entry for `rokkit skills` subcommands.
56
+ * @param {'list' | 'add'} sub
57
+ * @param {{ _?: string[], all?: boolean, force?: boolean }} [opts]
58
+ */
59
+ export function skillsCommand(sub: "list" | "add", opts?: {
60
+ _?: string[];
61
+ all?: boolean;
62
+ force?: boolean;
63
+ }): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/cli",
3
- "version": "1.1.12",
3
+ "version": "1.1.14",
4
4
  "description": "Command line utilities for icon bundling and conversion.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,6 +36,7 @@
36
36
  "files": [
37
37
  "lib/**/*",
38
38
  "src/**/*.js",
39
+ "skills/**",
39
40
  "dist/**/*.d.ts",
40
41
  "README.md",
41
42
  "package.json",
@@ -0,0 +1,256 @@
1
+ ---
2
+ name: command-system-rokkit
3
+ description: Use when adding keyboard shortcuts, a command palette, or app-level commands to a Rokkit (Svelte 5) app — the commands registry (@rokkit/states), the shortcuts action (@rokkit/actions), the CommandPalette component (@rokkit/ui), the 'mod+k' shortcut grammar, command scope/lifecycle, conflict detection, and i18n.
4
+ ---
5
+
6
+ # Rokkit Command System
7
+
8
+ Three packages, one coherent system — commands live in a registry, shortcuts fire them from the keyboard,
9
+ the palette lets users search and run them by name.
10
+
11
+ ```
12
+ @rokkit/states → commands registry (register, execute, resolve, commands.all)
13
+ @rokkit/actions → shortcuts action (use:shortcuts={commands})
14
+ @rokkit/ui → CommandPalette component (<CommandPalette bind:open />)
15
+ ```
16
+
17
+ Parallel to the theme system: `vibe` (state) + `themable` (action) + `ThemeSwitcherToggle` (component).
18
+ Commands are the same shape — registry singleton + action wiring + a ready-made UI.
19
+
20
+ ---
21
+
22
+ ## The three pieces
23
+
24
+ | Package | Export | Role |
25
+ |---------|--------|------|
26
+ | `@rokkit/states` | `commands` | Singleton registry — register, resolve, execute |
27
+ | `@rokkit/actions` | `shortcuts` | Svelte action — intercepts `keydown`, delegates to `commands` |
28
+ | `@rokkit/ui` | `CommandPalette` | Searchable dialog — reads `commands.all`, opens on `mod+k` |
29
+
30
+ ---
31
+
32
+ ## Defining commands
33
+
34
+ A `Command` object has one required field (`run`) plus metadata:
35
+
36
+ ```ts
37
+ type Command = {
38
+ id: string // unique — re-registering the same id replaces and warns
39
+ label: string // display text in the palette (consumer-localized)
40
+ run: () => void // executed on shortcut match or palette selection
41
+ shortcut?: string // e.g. 'mod+k', 'mod+shift+l', '?'
42
+ group?: string // palette category header
43
+ global?: boolean // fire even while a text input is focused (default false)
44
+ enabled?: () => boolean // dynamic gate — command is skipped when returns false
45
+ keywords?: string[] // extra palette search terms
46
+ }
47
+ ```
48
+
49
+ Register a batch with `registerMany` (returns a single unregister callback):
50
+
51
+ ```js
52
+ import { commands } from '@rokkit/states'
53
+
54
+ const unregister = commands.registerMany([
55
+ {
56
+ id: 'palette.open',
57
+ label: 'Open command palette',
58
+ shortcut: 'mod+k',
59
+ global: true, // fires while typing — required for Cmd/Ctrl+K
60
+ group: 'general',
61
+ run: () => { paletteOpen = true }
62
+ },
63
+ {
64
+ id: 'theme.toggle',
65
+ label: 'Toggle light / dark',
66
+ shortcut: 'mod+shift+l',
67
+ group: 'theme',
68
+ run: () => { vibe.mode = vibe.mode === 'dark' ? 'light' : 'dark' }
69
+ },
70
+ {
71
+ id: 'conversation.new',
72
+ label: 'New conversation',
73
+ shortcut: 'mod+shift+n',
74
+ group: 'general',
75
+ run: startNewConversation
76
+ }
77
+ ])
78
+ ```
79
+
80
+ Register one command with `register` (also returns an unregister callback):
81
+
82
+ ```js
83
+ const unregister = commands.register({
84
+ id: 'search.focus',
85
+ label: 'Focus search',
86
+ shortcut: '/',
87
+ run: () => searchInput?.focus()
88
+ })
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Wiring it up
94
+
95
+ Wire the root element once (e.g. `+layout.svelte`). Register commands in `onMount`
96
+ and return the unregister fn — Svelte calls it on destroy to clean up:
97
+
98
+ ```svelte
99
+ <script>
100
+ import { onMount } from 'svelte'
101
+ import { commands, vibe } from '@rokkit/states'
102
+ import { shortcuts } from '@rokkit/actions'
103
+ import { CommandPalette } from '@rokkit/ui'
104
+
105
+ let paletteOpen = $state(false)
106
+
107
+ onMount(() => {
108
+ return commands.registerMany([
109
+ {
110
+ id: 'palette.open',
111
+ label: 'Open command palette',
112
+ shortcut: 'mod+k',
113
+ global: true,
114
+ group: 'general',
115
+ run: () => { paletteOpen = true }
116
+ },
117
+ {
118
+ id: 'theme.toggle',
119
+ label: 'Toggle light / dark',
120
+ shortcut: 'mod+shift+l',
121
+ group: 'theme',
122
+ run: () => { vibe.mode = vibe.mode === 'dark' ? 'light' : 'dark' }
123
+ }
124
+ ])
125
+ })
126
+ </script>
127
+
128
+ <!-- shortcuts action on the root element intercepts keydown globally -->
129
+ <div use:shortcuts={commands}>
130
+ {@render children()}
131
+ </div>
132
+
133
+ <!-- palette renders as an overlay; it reads commands.all directly -->
134
+ <CommandPalette bind:open={paletteOpen} />
135
+ ```
136
+
137
+ The `shortcuts` action listens on `window` for `keydown` (via Svelte's `on()`), so
138
+ placing it on any element in the tree works — root element or `<svelte:body>` are both fine.
139
+
140
+ ---
141
+
142
+ ## Scope & lifecycle
143
+
144
+ **App-root commands** — register once in the root layout's `onMount`. They live for
145
+ the entire session. The `palette.open` command belongs here; it must always be reachable.
146
+
147
+ **Route or component-scoped commands** — register in the component's `onMount` and
148
+ return the unregister fn. They are removed when the component unmounts:
149
+
150
+ ```svelte
151
+ <script>
152
+ import { onMount } from 'svelte'
153
+ import { commands } from '@rokkit/states'
154
+
155
+ onMount(() => {
156
+ // These commands are only active while this component is mounted
157
+ return commands.registerMany([
158
+ {
159
+ id: 'editor.save',
160
+ label: 'Save document',
161
+ shortcut: 'mod+s',
162
+ group: 'editor',
163
+ run: saveDocument
164
+ },
165
+ {
166
+ id: 'editor.format',
167
+ label: 'Format code',
168
+ shortcut: 'mod+shift+f',
169
+ group: 'editor',
170
+ run: formatCode
171
+ }
172
+ ])
173
+ })
174
+ </script>
175
+ ```
176
+
177
+ `commands.all` is reactive (`$state` inside the registry), so `CommandPalette` and
178
+ any list reading `commands.all` automatically re-renders when commands are added or removed.
179
+
180
+ ---
181
+
182
+ ## Shortcut grammar
183
+
184
+ `mod` resolves to `Cmd` on macOS, `Ctrl` on Linux/Windows — use it for all
185
+ primary shortcuts so they are cross-platform by default.
186
+
187
+ | Shortcut string | Fires on |
188
+ |-----------------|----------|
189
+ | `'mod+k'` | Cmd+K (macOS) / Ctrl+K (other) |
190
+ | `'mod+shift+l'` | Cmd+Shift+L / Ctrl+Shift+L |
191
+ | `'mod+shift+n'` | Cmd+Shift+N / Ctrl+Shift+N |
192
+ | `'?'` | `?` key (no modifier) |
193
+ | `'alt+t'` | Option+T (macOS) / Alt+T (other) |
194
+
195
+ **`global: true` is required for any shortcut that must fire while a text input is focused.**
196
+ Without it the `shortcuts` action skips the command when `document.activeElement` is an
197
+ `<input>`, `<textarea>`, `<select>`, or `contenteditable`. The `palette.open` command
198
+ (`mod+k`) must always be `global: true` — the palette is most useful when the composer
199
+ or a form field is focused.
200
+
201
+ **Conflict detection:** the registry normalizes shortcuts on registration (modifiers
202
+ sorted by canonical order, key lowercased, `mod` resolved). If a second command registers
203
+ the same normalized shortcut it is warned and skipped — the first registrant wins.
204
+
205
+ **Single chords only:** the grammar is one key + modifiers. Multi-chord sequences
206
+ (e.g. Vim-style `g g`) are not supported.
207
+
208
+ ---
209
+
210
+ ## i18n
211
+
212
+ The palette chrome comes from `messages.command.*` — override once to localize globally:
213
+
214
+ ```js
215
+ import { messages } from '@rokkit/states'
216
+
217
+ messages.set({
218
+ command: {
219
+ placeholder: 'Befehl ausführen…',
220
+ noResults: 'Keine Befehle gefunden',
221
+ label: 'Befehlspalette'
222
+ }
223
+ })
224
+ ```
225
+
226
+ Or register a named locale and switch:
227
+
228
+ ```js
229
+ messages.register('de', {
230
+ command: {
231
+ placeholder: 'Befehl ausführen…',
232
+ noResults: 'Keine Befehle gefunden',
233
+ label: 'Befehlspalette'
234
+ }
235
+ })
236
+ messages.setLocale('de')
237
+ ```
238
+
239
+ Command `label` and `keywords` are consumer-supplied strings — localize them
240
+ directly in the object passed to `register` / `registerMany`.
241
+
242
+ Default English values: `placeholder: 'Run a command…'`, `noResults: 'No commands found'`,
243
+ `label: 'Command palette'`.
244
+
245
+ ---
246
+
247
+ ## Common mistakes
248
+
249
+ | Mistake | Why it fails | Fix |
250
+ |---------|-------------|-----|
251
+ | Registering at module top-level (not in `onMount`) | The registry is a singleton; commands pile up across hot-reloads in dev and are never cleaned up on unmount | Always register in `onMount`, return the unregister fn |
252
+ | Forgetting `global: true` on `mod+k` (or any Cmd-shortcut) | `shortcuts` skips non-global commands while a text input is focused — Cmd+K fired in the composer silently no-ops | Add `global: true` to any command that must fire during text input |
253
+ | Hand-rolling `window.addEventListener('keydown', ...)` | No conflict detection, no `enabled()` gate, no `commands.all` integration, no cleanup | Use `use:shortcuts={commands}` — it handles all of these |
254
+ | Using `<svelte:body use:shortcuts />` without noting that `shortcuts` uses `window` | The action attaches a `window` listener regardless of which element it's placed on — placing it on body is fine but redundant | Place on any element in the tree; root element or body both work |
255
+ | Putting `run: () => paletteOpen = true` at module scope | `paletteOpen` is `$state` in the component — the closure captures the right reference only when created inside the component function | Define `run` functions after `$state` declarations, inside the component |
256
+ | Expecting `commands.all` to include disabled commands without `enabled` | `enabled` is a dynamic gate called at dispatch time, not a filter on the list | `commands.all` always contains all registered commands; `enabled` only gates execution |