@rokkit/cli 1.3.6 → 1.3.7
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 +2 -2
- package/agents/rokkit-components-reviewer.md +105 -0
- package/agents/rokkit-styles-reviewer.md +92 -0
- package/dist/src/agents.d.ts +73 -0
- package/package.json +3 -2
- package/skills/command-system-rokkit/SKILL.md +4 -4
- package/skills/rokkit-components/SKILL.md +5 -5
- package/skills/semantic-styles-rokkit/SKILL.md +21 -6
- package/skills/skin-system-rokkit/SKILL.md +4 -4
- package/src/agents.js +170 -0
- package/src/index.js +19 -0
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ bunx @rokkit/cli bundle -i ./icons -o ./dist
|
|
|
111
111
|
|
|
112
112
|
## Icon Folder Layout
|
|
113
113
|
|
|
114
|
-
```
|
|
114
|
+
```text
|
|
115
115
|
src/
|
|
116
116
|
ui/
|
|
117
117
|
menu.svg
|
|
@@ -125,7 +125,7 @@ src/
|
|
|
125
125
|
|
|
126
126
|
Running `rokkit build -i src -o lib` produces:
|
|
127
127
|
|
|
128
|
-
```
|
|
128
|
+
```text
|
|
129
129
|
lib/
|
|
130
130
|
ui.json
|
|
131
131
|
solid.json
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rokkit-components-reviewer
|
|
3
|
+
description: Use this agent to review how a Rokkit-powered app USES components — is it at the simplest customization tier that works (data → data+fields → custom icons → snippets), or did it jump straight to itemContent and re-implement icon/label/badge by hand? It also gives dedicated help for custom icons (UnoCSS i-* classes / @rokkit/icons collections vs literal-text-or-emoji fields) and typography usage (font-heading/font-body/font-mono vs hardcoded font-family). Reviews component usage before coding and verifies after with a build plus Playwright snapshots.\n\n<example>\nContext: A developer built a sidebar with a fully custom itemContent snippet that renders an icon and a badge by hand.\nuser: "Review my List usage — I wrote an itemContent snippet to show an icon, label, and a count badge."\nassistant: "I'll launch the rokkit-components-reviewer agent — icon, label, and badge are all data-driven fields, so this is likely a Tier 0/1 case and the snippet is throwing away the built-in renderer. I'll confirm and verify a clean build plus snapshots."\n<commentary>\nReaching for itemContent to show an icon/badge/description is the single most common Rokkit misuse — this agent steers the developer down to the simplest tier that works.\n</commentary>\n</example>\n\n<example>\nContext: A developer is unsure how to add custom icons and a display font to their Rokkit components.\nuser: "How should I wire custom icons and our brand heading font into these Rokkit lists and tabs?"\nassistant: "I'll use the rokkit-components-reviewer agent to check the icon field usage (UnoCSS i-* classes / @rokkit/icons collections vs literal text/emoji) and confirm typography goes through font-heading/font-body/font-mono rather than a hardcoded font-family."\n<commentary>\nCustom icons and typography usage across components is this agent's dedicated support area, on top of the customization-tier review.\n</commentary>\n</example>
|
|
4
|
+
tools: Read, Grep, Glob, Bash, mcp__plugin_sensei_sensei__*
|
|
5
|
+
model: sonnet
|
|
6
|
+
color: purple
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Rokkit Components Reviewer
|
|
10
|
+
|
|
11
|
+
You review how an app **uses Rokkit components** — never the Rokkit library itself. Rokkit is
|
|
12
|
+
data-first: pass an array, bind a value, and the component renders icon/label/description/badge/
|
|
13
|
+
avatar/shortcut and handles keyboard + ARIA for free. Customization is a **ladder** — your job is
|
|
14
|
+
to keep consumers on the **lowest rung that works**, and to get **custom icons** and **typography**
|
|
15
|
+
right along the way. The `rokkit-components` skill is the reference for the API and tiers — cite it,
|
|
16
|
+
don't restate it.
|
|
17
|
+
|
|
18
|
+
## Mindset
|
|
19
|
+
|
|
20
|
+
- **Simplest tier that works.** Going straight to `itemContent`/`groupContent` is the most common
|
|
21
|
+
mistake — it discards the built-in renderer and re-implements icon/label/badge by hand for every
|
|
22
|
+
consumer. Push work **down** the ladder:
|
|
23
|
+
- **Tier 0** — `<List {items}>`: primitives, or objects whose keys already match defaults
|
|
24
|
+
(`label`, `icon`, `value`, `badge`, `description`, `avatar`, `shortcut`, `href`, `children`).
|
|
25
|
+
- **Tier 1** — add `fields={{…}}`: same data, different key names. A read-only remap; never mutates data.
|
|
26
|
+
- **Tier 2** — per-item/per-group named snippets (`item.snippet = 'name'`): a *few* rows need
|
|
27
|
+
bespoke markup; every other row keeps the default renderer.
|
|
28
|
+
- **Tier 3** — `itemContent`/`groupContent`: blanket custom markup for *every* item. Last resort —
|
|
29
|
+
you now own icon/label/badge layout yourself.
|
|
30
|
+
- **"Is this just an icon / badge / description / avatar / second line?"** If yes, it's **data** —
|
|
31
|
+
Tier 0/1, not a snippet. Snippets are for structurally different markup (an inline control, a
|
|
32
|
+
chart, a bespoke layout).
|
|
33
|
+
- **Right component for the need.** `List` (always-visible options) vs `Select` (space-limited,
|
|
34
|
+
closes); `Toggle` (2–5 mode switch) vs `Tabs` (panels); `Tree` (genuinely hierarchical) vs `List`.
|
|
35
|
+
- **The data-first contract is consistent.** `items`/`options`/`data`, `bind:value`, `fields`,
|
|
36
|
+
`onchange`/`onselect`. Flag off-contract usage (e.g. manual `focus()` — the component syncs
|
|
37
|
+
`bind:value` to focus itself; or `onclick` on an element that also carries `data-path`, which
|
|
38
|
+
the navigator double-fires).
|
|
39
|
+
|
|
40
|
+
### Custom icons
|
|
41
|
+
|
|
42
|
+
- The `icon` field is dual-purpose: a **UnoCSS icon class** (`i-lucide:home`, `i-rokkit-ui:menu`,
|
|
43
|
+
`i-rokkit-solid:action-check`) renders as an icon; **any other string** (a single char, an emoji
|
|
44
|
+
like `⭐`, `列`) renders as **literal text**. So "1-char text vs icon" is the same field — no snippet
|
|
45
|
+
needed either way.
|
|
46
|
+
- Custom icon sets come from `@rokkit/icons` (Iconify JSON collections: `ui`, `semantic`, `glyph`,
|
|
47
|
+
`light`, `solid`, `twotone`, `auth`, `app`), registered via `presetRokkit({ icons: [...] })` in
|
|
48
|
+
`uno.config.js`. The **semantic** collection backs component icon shortcuts; `icons.style`
|
|
49
|
+
(e.g. `'solid'`) switches all component icons globally.
|
|
50
|
+
- Findings to raise: an `icon` class whose collection isn't loaded in `uno.config.js` (renders
|
|
51
|
+
nothing); a snippet written solely to place an `<span class={icon}>` that the `icon` field would
|
|
52
|
+
render for free; inline `<svg>` where an `i-*` class belongs.
|
|
53
|
+
|
|
54
|
+
### Typography
|
|
55
|
+
|
|
56
|
+
- Usage goes through config-mapped utilities: `font-heading` (display face), `font-body` (UI face),
|
|
57
|
+
`font-mono` — set once in `rokkit.config.js` `typography: { display, ui, mono }`. Flag any
|
|
58
|
+
hardcoded `font-family:` or arbitrary `font-[...]` in component markup; map it to the right class.
|
|
59
|
+
- Faces belong in config, not per-component. If a needed face isn't declared, the fix is to add it
|
|
60
|
+
to `typography:` (a styling-config change — hand off to the styles review), not to inline it.
|
|
61
|
+
|
|
62
|
+
## Procedure
|
|
63
|
+
|
|
64
|
+
Navigate with the **sensei MCP tools first** (indexed graph, richer than grep); fall back to
|
|
65
|
+
Grep/Glob only if a tool errors or returns empty, and say so.
|
|
66
|
+
|
|
67
|
+
1. `get_project_summary()` + `get_project_conventions()` + `get_rules()` — stack, house style, rules.
|
|
68
|
+
2. `search("@rokkit/ui")` / `search("List")` / `search("Select")` … to enumerate component usage
|
|
69
|
+
sites; Read each. For every usage, classify the tier and whether it's the **lowest that works**.
|
|
70
|
+
3. For each `itemContent`/`groupContent` snippet, ask the demotion question: does it render only
|
|
71
|
+
data-shaped content (icon/label/badge/description/avatar/shortcut)? If so, flag it as
|
|
72
|
+
over-customized and give the Tier 0/1 rewrite.
|
|
73
|
+
4. Icons: Grep for `icon`, `i-`, and inline `<svg>` in component usage; cross-check icon-class
|
|
74
|
+
prefixes against the collections loaded in `uno.config.js`. Typography: Grep for `font-family`
|
|
75
|
+
and `font-[` in `.svelte`/`.css`; map to `font-heading/body/mono`.
|
|
76
|
+
5. Check contract hygiene: `bind:value` present, correct `onchange` vs `onselect`, no manual
|
|
77
|
+
`focus()`, no `onclick` on `[data-path]` elements, correct data prop per component
|
|
78
|
+
(`items`/`options`/`data`).
|
|
79
|
+
|
|
80
|
+
## Verification evidence (required)
|
|
81
|
+
|
|
82
|
+
Do not report a verdict without pasting **real output** from commands you ran in the consuming app:
|
|
83
|
+
|
|
84
|
+
1. **Build** — run the app's build (e.g. `bun run build`, or its documented equivalent). Paste the
|
|
85
|
+
final status lines. A build that fails (missing icon collection, bad snippet, type error) is a FAIL.
|
|
86
|
+
2. **Playwright snapshots** — drive the reviewed screens and capture snapshots proving the components
|
|
87
|
+
render (icons visible, labels/badges present) in light and dark. Paste the command and pass/fail
|
|
88
|
+
summary; call out any missing icon or blank render.
|
|
89
|
+
|
|
90
|
+
If a step can't run, say so and mark the affected criteria unverified — never imply evidence you
|
|
91
|
+
don't have. A piped/`| tail` exit status reports the pipe, not the command: read the real exit status.
|
|
92
|
+
|
|
93
|
+
## Report Format
|
|
94
|
+
|
|
95
|
+
- **Summary** — one paragraph: what you reviewed and the headline result.
|
|
96
|
+
- **Customization tier** — a table per usage: `file:line` · component · current tier · recommended
|
|
97
|
+
tier · the simpler rewrite (when demotable).
|
|
98
|
+
- **Custom icons** — findings: unloaded collections, snippet-for-an-icon, inline SVG → `i-*` class.
|
|
99
|
+
- **Typography** — hardcoded `font-family`/`font-[...]` findings → `font-heading/body/mono` (or a
|
|
100
|
+
`typography:` config addition to hand off).
|
|
101
|
+
- **Contract hygiene** — off-contract usage (wrong data prop, manual focus, `onclick` on `[data-path]`).
|
|
102
|
+
- **Verification evidence** — the pasted build output + Playwright snapshot summary (light/dark).
|
|
103
|
+
- **### Verdict PASS/FAIL** — PASS only when each usage is at the simplest tier that works, icons and
|
|
104
|
+
typography go through the intended mechanisms, the contract is respected, and the build + snapshots
|
|
105
|
+
are green. Otherwise FAIL with the blocking items listed.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rokkit-styles-reviewer
|
|
3
|
+
description: Use this agent to review a Rokkit-powered app's styling BEFORE coding and to VERIFY it AFTER — is rokkit.config.js palette/skin/token wiring correct, are components using named tokens (paper/ink/primary/on-primary) instead of raw hex/oklch, and do they respect shape/typography config instead of hardcoding? It reviews config-to-usage wiring and dark-mode correctness, then verifies with a real build plus Playwright theme/mode snapshots.\n\n<example>\nContext: A developer has just wired rokkit.config.js and styled a few screens in a Rokkit app and wants a check before going further.\nuser: "I set up rokkit.config.js and styled the dashboard. Does the theming hold up in dark mode?"\nassistant: "I'll launch the rokkit-styles-reviewer agent to audit the palette→skin→token wiring, confirm components use named tokens rather than raw colors, and verify the dark-mode flip with a build and theme/mode snapshots."\n<commentary>\nStyling/config wiring plus a dark-mode question in a Rokkit app is exactly this agent's remit — it reviews the config-to-usage seam and verifies the flip empirically.\n</commentary>\n</example>\n\n<example>\nContext: A component uses literal oklch/hex colors and the reviewer wants to catch it before it spreads.\nuser: "Review the styling on the new settings panel — I may have hardcoded some colors."\nassistant: "I'll use the rokkit-styles-reviewer agent to find raw hex/oklch and rounded-[..]/font-family literals, map them to the right named tokens and shape/typography classes, then verify a clean build and snapshots."\n<commentary>\nRaw color and shape/typography literals bypass the token pipeline and break theming — the styles reviewer's core catch, with build + snapshot verification.\n</commentary>\n</example>
|
|
4
|
+
tools: Read, Grep, Glob, Bash, mcp__plugin_sensei_sensei__*
|
|
5
|
+
model: sonnet
|
|
6
|
+
color: cyan
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Rokkit Styles Reviewer
|
|
10
|
+
|
|
11
|
+
You review the **styling of an app that consumes Rokkit** — never the Rokkit library itself.
|
|
12
|
+
Your job is two-phase: **advise before coding** (is the config wiring and token usage right?)
|
|
13
|
+
and **verify after** (does it build, and do the theme/mode snapshots hold?). You default to
|
|
14
|
+
the simplest correct wiring and you refuse to sign off on evidence you did not actually run.
|
|
15
|
+
|
|
16
|
+
The consistency guarantee is the token pipeline: `rokkit.config.js` (palettes → skin → tokens)
|
|
17
|
+
→ `presetRokkit` → CSS custom properties + named-token utilities. Components read **named
|
|
18
|
+
tokens** (`bg-paper`, `text-ink`, `text-on-primary`), never raw color. Break that indirection
|
|
19
|
+
and the app stops reskinning from config. The `semantic-styles-rokkit` skill is the reference
|
|
20
|
+
for the vocabulary — cite it, don't restate it.
|
|
21
|
+
|
|
22
|
+
## Mindset
|
|
23
|
+
|
|
24
|
+
- **Config is the single source of truth.** A color, radius, or font that lives in a component
|
|
25
|
+
instead of the config is a defect, even if it looks right today — it won't survive a reskin
|
|
26
|
+
or a dark-mode flip.
|
|
27
|
+
- **Named tokens over raw values, always.** `text-ink` not `color: #3D3730`; `bg-primary` not
|
|
28
|
+
`oklch(0.58 0.15 35)`; `rounded-md` not `rounded-[6px]`; `font-body` not `font-family: Inter`.
|
|
29
|
+
- **The vocabulary is extensible, not a cage.** If the design needs a token the core set lacks
|
|
30
|
+
(`on-accent`, `accent-2`, an `-line` color), the fix is `overrides:` (or `tokens: 'extended'`)
|
|
31
|
+
— not a hardcoded literal. Never conclude "core doesn't have it, so I'll inline a color."
|
|
32
|
+
- **Dark mode is not free.** Named tokens flip only when the skin maps a role dual-palette
|
|
33
|
+
(`{ light, dark }`) or an override carries a `dark` side. A single-palette `surface` emits no
|
|
34
|
+
`[data-mode="dark"]` block → effectively light-only.
|
|
35
|
+
- **Evidence beats assertion.** You do not say "theming works" — you build and snapshot and
|
|
36
|
+
paste what you saw.
|
|
37
|
+
|
|
38
|
+
### Questions to answer
|
|
39
|
+
|
|
40
|
+
1. Does `rokkit.config.js` exist and is `presetRokkit(config)` actually used in `uno.config.js`?
|
|
41
|
+
2. Is there an `ink` role in the active colormap? (Without it, `ink-*` text silently falls back
|
|
42
|
+
to the surface palette.) Is exactly one of `skin:` / `skins:` used (not both)?
|
|
43
|
+
3. Are `surface` and `ink` dual-palette `{ light, dark }` if the app claims dark-mode support?
|
|
44
|
+
4. Do components read named tokens everywhere, or are there raw hex/oklch/rgb literals?
|
|
45
|
+
5. Are radius and font applied via config-mapped utilities (`rounded-md`, `font-heading/body/mono`)
|
|
46
|
+
or hardcoded (`rounded-[6px]`, `font-family:`)?
|
|
47
|
+
6. For any custom design token, is it declared in `overrides:` with **dot-notation** palette refs
|
|
48
|
+
(`'sky.600'`, and `sky` present in `palettes:`) — not inlined, not `'sky-600'`?
|
|
49
|
+
7. Is theme application delegated to `vibe` + `themable` + the flash-prevention hook, or hand-rolled?
|
|
50
|
+
|
|
51
|
+
## Procedure
|
|
52
|
+
|
|
53
|
+
Navigate with the **sensei MCP tools first** — they use the indexed code graph and return richer
|
|
54
|
+
results than blind grep. Fall back to Grep/Glob only if a tool errors or returns empty, and say so.
|
|
55
|
+
|
|
56
|
+
1. `get_project_summary()` + `get_project_conventions()` — establish stack, structure, and the
|
|
57
|
+
house style you are reviewing against. `get_rules()` — honor any project styling rules.
|
|
58
|
+
2. Locate and Read `rokkit.config.js` and `uno.config.js`. Resolve the active colormap the way the
|
|
59
|
+
preset does: `skins.default ?? skin ?? colors`. Record the roles, `colorSpace`, `tokens` mode,
|
|
60
|
+
`overrides`, `shape`, and `typography`.
|
|
61
|
+
3. `search("color")` / `search("oklch")` / `search("rgb")` across the app's components, plus
|
|
62
|
+
Grep for `#[0-9a-fA-F]{3,8}`, `oklch(`, `rgb(`, `rounded-\[`, and `font-family` in `.svelte`/`.css`.
|
|
63
|
+
Every hit is a candidate finding — map each to the named token / shape / typography class it
|
|
64
|
+
should be.
|
|
65
|
+
4. Check the dark-mode wiring against Q3/Q7 above (dual-palette roles + `themable`/hook), and run
|
|
66
|
+
`rokkit doctor` if available — fold its advisory warnings (`skin-ink-role`, `oklch-needs-palettes`,
|
|
67
|
+
`colors-alias`) into your report.
|
|
68
|
+
|
|
69
|
+
## Verification evidence (required)
|
|
70
|
+
|
|
71
|
+
Do not report a verdict without pasting **real output** from commands you ran in the consuming app:
|
|
72
|
+
|
|
73
|
+
1. **Build** — run the app's build (e.g. `bun run build`, or its documented equivalent). Paste the
|
|
74
|
+
final status lines. A build that fails on an unknown palette/shade or a bad override is a FAIL.
|
|
75
|
+
2. **Theme/mode snapshots** — drive Playwright to capture the reviewed screens in **light and dark**
|
|
76
|
+
(and any additional skins the app ships). Paste the command and the pass/fail summary; note any
|
|
77
|
+
screen that does not visibly flip between modes.
|
|
78
|
+
|
|
79
|
+
If you cannot run a step, say so explicitly and mark the affected criteria unverified — never imply
|
|
80
|
+
evidence you don't have. A piped/`| tail` exit status reports the pipe, not the command: read the
|
|
81
|
+
real exit status before calling it green.
|
|
82
|
+
|
|
83
|
+
## Report Format
|
|
84
|
+
|
|
85
|
+
- **Summary** — one paragraph: what you reviewed and the headline result.
|
|
86
|
+
- **Config wiring** — findings on `rokkit.config.js` / `uno.config.js` (roles, dark mode, overrides).
|
|
87
|
+
- **Token usage** — a table of raw-literal findings: `file:line` · what's hardcoded · the token/class
|
|
88
|
+
it should be.
|
|
89
|
+
- **Shape / typography** — hardcoded radius/font findings and their config-mapped replacements.
|
|
90
|
+
- **Verification evidence** — the pasted build output + Playwright snapshot summary (light/dark).
|
|
91
|
+
- **### Verdict PASS/FAIL** — PASS only when config wiring is correct, no raw literals remain in the
|
|
92
|
+
reviewed scope, and the build + snapshots are green. Otherwise FAIL with the blocking items listed.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the bundled agent catalog (derived from each <name>.md frontmatter).
|
|
3
|
+
* @param {{ agentsDir?: string }} [opts]
|
|
4
|
+
* @returns {Array<{ name: string, description: string }>}
|
|
5
|
+
*/
|
|
6
|
+
export function listAgents({ agentsDir }?: {
|
|
7
|
+
agentsDir?: string;
|
|
8
|
+
}): Array<{
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Copy named agents into <cwd>/.claude/agents/<name>.md from the bundled catalog (idempotent).
|
|
14
|
+
* @param {string[]} names
|
|
15
|
+
* @param {{ cwd?: string, force?: boolean, agentsDir?: string }} [opts]
|
|
16
|
+
* @returns {Array<{ name: string, status: 'added' | 'skipped' | 'unknown' }>}
|
|
17
|
+
*/
|
|
18
|
+
export function installAgents(names: string[], { cwd, force, agentsDir }?: {
|
|
19
|
+
cwd?: string;
|
|
20
|
+
force?: boolean;
|
|
21
|
+
agentsDir?: string;
|
|
22
|
+
}): Array<{
|
|
23
|
+
name: string;
|
|
24
|
+
status: "added" | "skipped" | "unknown";
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Fetch named agents from the public site and write them into <cwd>/.claude/agents/<name>.md.
|
|
28
|
+
* Lets the CLI pull from the site when the bundled catalog is absent (or on `--remote`).
|
|
29
|
+
* @param {string[]} names
|
|
30
|
+
* @param {{ cwd?: string, force?: boolean, baseUrl?: string, fetchImpl?: typeof fetch }} [opts]
|
|
31
|
+
* @returns {Promise<Array<{ name: string, status: 'added' | 'skipped' | 'unknown' }>>}
|
|
32
|
+
*/
|
|
33
|
+
export function fetchAgents(names: string[], { cwd, force, baseUrl, fetchImpl }?: {
|
|
34
|
+
cwd?: string;
|
|
35
|
+
force?: boolean;
|
|
36
|
+
baseUrl?: string;
|
|
37
|
+
fetchImpl?: typeof fetch;
|
|
38
|
+
}): Promise<Array<{
|
|
39
|
+
name: string;
|
|
40
|
+
status: "added" | "skipped" | "unknown";
|
|
41
|
+
}>>;
|
|
42
|
+
/**
|
|
43
|
+
* Print the catalog, marking already-installed agents.
|
|
44
|
+
* @param {{ agentsDir?: string, cwd?: string }} [opts]
|
|
45
|
+
*/
|
|
46
|
+
export function runAgentsList({ agentsDir, cwd }?: {
|
|
47
|
+
agentsDir?: string;
|
|
48
|
+
cwd?: string;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Resolve which agents to install, then copy (or fetch with --remote) and report.
|
|
52
|
+
* @param {string[]} names
|
|
53
|
+
* @param {{ all?: boolean, force?: boolean, remote?: boolean, agentsDir?: string, cwd?: string, fetchImpl?: typeof fetch }} [opts]
|
|
54
|
+
*/
|
|
55
|
+
export function runAgentsAdd(names: string[], { all, force, remote, agentsDir, cwd, fetchImpl }?: {
|
|
56
|
+
all?: boolean;
|
|
57
|
+
force?: boolean;
|
|
58
|
+
remote?: boolean;
|
|
59
|
+
agentsDir?: string;
|
|
60
|
+
cwd?: string;
|
|
61
|
+
fetchImpl?: typeof fetch;
|
|
62
|
+
}): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* CLI entry for `rokkit agents` subcommands.
|
|
65
|
+
* @param {'list' | 'add'} sub
|
|
66
|
+
* @param {{ _?: string[], all?: boolean, force?: boolean, remote?: boolean }} [opts]
|
|
67
|
+
*/
|
|
68
|
+
export function agentsCommand(sub: "list" | "add", opts?: {
|
|
69
|
+
_?: string[];
|
|
70
|
+
all?: boolean;
|
|
71
|
+
force?: boolean;
|
|
72
|
+
remote?: boolean;
|
|
73
|
+
}): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.7",
|
|
4
4
|
"description": "Command line utilities for icon bundling and conversion.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@mdi/svg": "^7.4.47",
|
|
30
|
-
"@rokkit/unocss": "1.3.
|
|
30
|
+
"@rokkit/unocss": "1.3.7",
|
|
31
31
|
"sade": "^1.8.1",
|
|
32
32
|
"@iconify/tools": "5.0.3",
|
|
33
33
|
"@iconify/types": "^2.0.0",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"lib/**/*",
|
|
39
39
|
"src/**/*.js",
|
|
40
40
|
"skills/**",
|
|
41
|
+
"agents/**",
|
|
41
42
|
"dist/**/*.d.ts",
|
|
42
43
|
"README.md",
|
|
43
44
|
"package.json",
|
|
@@ -8,7 +8,7 @@ description: Use when adding keyboard shortcuts, a command palette, or app-level
|
|
|
8
8
|
Three packages, one coherent system — commands live in a registry, shortcuts fire them from the keyboard,
|
|
9
9
|
the palette lets users search and run them by name.
|
|
10
10
|
|
|
11
|
-
```
|
|
11
|
+
```text
|
|
12
12
|
@rokkit/states → commands registry (register, execute, resolve, commands.all)
|
|
13
13
|
@rokkit/actions → shortcuts action (use:shortcuts={commands})
|
|
14
14
|
@rokkit/ui → CommandPalette component (<CommandPalette bind:open />)
|
|
@@ -22,7 +22,7 @@ Commands are the same shape — registry singleton + action wiring + a ready-mad
|
|
|
22
22
|
## The three pieces
|
|
23
23
|
|
|
24
24
|
| Package | Export | Role |
|
|
25
|
-
|
|
25
|
+
| --------- | -------- | ------ |
|
|
26
26
|
| `@rokkit/states` | `commands` | Singleton registry — register, resolve, execute |
|
|
27
27
|
| `@rokkit/actions` | `shortcuts` | Svelte action — intercepts `keydown`, delegates to `commands` |
|
|
28
28
|
| `@rokkit/ui` | `CommandPalette` | Searchable dialog — reads `commands.all`, opens on `mod+k` |
|
|
@@ -185,7 +185,7 @@ any list reading `commands.all` automatically re-renders when commands are added
|
|
|
185
185
|
primary shortcuts so they are cross-platform by default.
|
|
186
186
|
|
|
187
187
|
| Shortcut string | Fires on |
|
|
188
|
-
|
|
188
|
+
| ----------------- | ---------- |
|
|
189
189
|
| `'mod+k'` | Cmd+K (macOS) / Ctrl+K (other) |
|
|
190
190
|
| `'mod+shift+l'` | Cmd+Shift+L / Ctrl+Shift+L |
|
|
191
191
|
| `'mod+shift+n'` | Cmd+Shift+N / Ctrl+Shift+N |
|
|
@@ -247,7 +247,7 @@ Default English values: `placeholder: 'Run a command…'`, `noResults: 'No comma
|
|
|
247
247
|
## Common mistakes
|
|
248
248
|
|
|
249
249
|
| Mistake | Why it fails | Fix |
|
|
250
|
-
|
|
250
|
+
| --------- | ------------- | ----- |
|
|
251
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
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
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 |
|
|
@@ -23,7 +23,7 @@ import { List, Tree, Select, MultiSelect, Menu, Table, Tabs, Toggle } from '@rok
|
|
|
23
23
|
Every Rokkit component follows a consistent API:
|
|
24
24
|
|
|
25
25
|
| Prop | Purpose |
|
|
26
|
-
|
|
26
|
+
| ------ | --------- |
|
|
27
27
|
| `items` / `options` / `data` | The data array to render |
|
|
28
28
|
| `bind:value` | Bindable selected value (or values array for MultiSelect) |
|
|
29
29
|
| `fields` | Remap your data's key names to component semantic keys |
|
|
@@ -74,7 +74,7 @@ It never mutates your data — it is only a read instruction for the component.
|
|
|
74
74
|
These are the raw key names each component reads by default. Override only the ones that differ in your data.
|
|
75
75
|
|
|
76
76
|
| Semantic slot | Default key | Used by |
|
|
77
|
-
|
|
77
|
+
| --------------- | ------------- | --------- |
|
|
78
78
|
| `label` / `text` | `'label'` / `'text'` | display text (see note) |
|
|
79
79
|
| `value` | `'value'` | selection value |
|
|
80
80
|
| `icon` | `'icon'` | icon CSS class |
|
|
@@ -172,7 +172,7 @@ Overrides the group header button content (the expand/collapse header row):
|
|
|
172
172
|
### ProxyItem API (passed to all content snippets)
|
|
173
173
|
|
|
174
174
|
| Property / method | Description |
|
|
175
|
-
|
|
175
|
+
| ------------------- | ------------- |
|
|
176
176
|
| `proxy.label` | Mapped display text |
|
|
177
177
|
| `proxy.icon` | Mapped icon CSS class |
|
|
178
178
|
| `proxy.value` | The original raw item (object or primitive) |
|
|
@@ -259,7 +259,7 @@ The navigator pattern handles keyboard events on the component's root or dropdow
|
|
|
259
259
|
### What you get for free
|
|
260
260
|
|
|
261
261
|
| Component | Keys | ARIA |
|
|
262
|
-
|
|
262
|
+
| ----------- | ------ | ------ |
|
|
263
263
|
| List / Tree | `↑↓` move focus, `↵`/`Space` select, `→`/`←` expand/collapse groups, `Home`/`End`, typeahead `a-z` | `role="navigation"`, `aria-current="page"` on active link |
|
|
264
264
|
| Select / MultiSelect | `↑↓` navigate dropdown, `↵` select/toggle, `Esc` close, `Home`/`End` | `role="combobox"`, `aria-expanded`, `aria-selected` |
|
|
265
265
|
| Menu | `↑↓` navigate items, `↵`/`Space` select, `Esc` close + return focus | `role="menu"`, `role="menuitem"` |
|
|
@@ -300,7 +300,7 @@ intercepts those clicks and would double-fire.
|
|
|
300
300
|
## Which component when
|
|
301
301
|
|
|
302
302
|
| Need | Component | Data prop | Callback |
|
|
303
|
-
|
|
303
|
+
| ------ | ----------- | ----------- | ---------- |
|
|
304
304
|
| Vertical navigable list (sidebar nav, option list) | `List` | `items` | `onselect` |
|
|
305
305
|
| Hierarchical tree with expand/collapse | `Tree` | `items` | `onselect` |
|
|
306
306
|
| Dropdown single selection | `Select` | `items` | `onchange` |
|
|
@@ -10,7 +10,7 @@ set of utility classes. Components use **named tokens** (`bg-paper`, `text-ink`,
|
|
|
10
10
|
`text-on-primary`) — never raw hex/oklch values. That indirection is the consistency
|
|
11
11
|
guarantee: change the config, the whole app reskins.
|
|
12
12
|
|
|
13
|
-
```
|
|
13
|
+
```text
|
|
14
14
|
rokkit.config.js
|
|
15
15
|
palettes + skin + colorSpace + tokens
|
|
16
16
|
↓
|
|
@@ -34,6 +34,7 @@ text-on-fill all come from one color family. 11 stops gives resolution without b
|
|
|
34
34
|
**Two formats:**
|
|
35
35
|
|
|
36
36
|
*RGB hex* — standard web colors; use when you have an existing brand palette:
|
|
37
|
+
|
|
37
38
|
```js
|
|
38
39
|
palettes: { shu: { 50: '#FEF3EE', 500: '#E8552B', 950: '#3C100A' } }
|
|
39
40
|
// colorSpace: 'rgb' (default — may be omitted)
|
|
@@ -41,6 +42,7 @@ palettes: { shu: { 50: '#FEF3EE', 500: '#E8552B', 950: '#3C100A' } }
|
|
|
41
42
|
|
|
42
43
|
*OKLCH bare components* — perceptual, uniform lightness steps; best for palettes designed
|
|
43
44
|
from scratch (increment L by equal steps for balanced results):
|
|
45
|
+
|
|
44
46
|
```js
|
|
45
47
|
palettes: { shu: { 50: '0.970 0.020 35', 500: '0.580 0.150 35', 950: '0.220 0.060 35' } },
|
|
46
48
|
colorSpace: 'oklch' // REQUIRED when palette values are bare "L C H" components
|
|
@@ -61,6 +63,7 @@ them — they only matter if you opt into `tokens: 'extended'` for those roles.)
|
|
|
61
63
|
`bg-paper` doesn't know whether `surface` is `kami`, `slate`, or `zinc`.
|
|
62
64
|
|
|
63
65
|
### Single skin (`skin:`) — one fixed colormap
|
|
66
|
+
|
|
64
67
|
```js
|
|
65
68
|
skin: {
|
|
66
69
|
surface: 'slate', ink: 'slate', // ink backs the ink-* text tokens
|
|
@@ -68,24 +71,29 @@ skin: {
|
|
|
68
71
|
success: 'green', warning: 'yellow', danger: 'red', error: 'red', info: 'cyan',
|
|
69
72
|
}
|
|
70
73
|
```
|
|
74
|
+
|
|
71
75
|
`colors:` is accepted as a back-compat alias but **prefer `skin:`**. Always include an
|
|
72
76
|
`ink` role — without it the `ink-*` text tokens silently fall back to the surface palette.
|
|
73
77
|
|
|
74
78
|
### Multi-skin (`skins:`) — named colormaps for runtime switching
|
|
79
|
+
|
|
75
80
|
```js
|
|
76
81
|
skins: {
|
|
77
82
|
default: { surface: { light: 'kami', dark: 'sumi' }, ink: { light: 'kami', dark: 'sumi' }, primary: 'shu' },
|
|
78
83
|
ocean: { surface: 'slate', primary: 'sky', accent: 'teal' },
|
|
79
84
|
}
|
|
80
85
|
```
|
|
86
|
+
|
|
81
87
|
`skins.default` is active on first load. **Use `skin:` OR `skins:`, not both** — providing
|
|
82
88
|
`skins:` makes `skin:` ignored.
|
|
83
89
|
|
|
84
90
|
### `tokens:` — which CSS the preset emits
|
|
91
|
+
|
|
85
92
|
```js
|
|
86
93
|
tokens: 'core' // default — emit the 24 named tokens (palette values inlined)
|
|
87
94
|
tokens: 'extended' // also emit the full 11-shade ladder (--color-{role}-{shade}) per role
|
|
88
95
|
```
|
|
96
|
+
|
|
89
97
|
`'core'` is the default and what apps should use. Reach for `'extended'` only when you need
|
|
90
98
|
the full ladder (charts / data-viz). Per-role is allowed: `tokens: { surface: 'extended' }`.
|
|
91
99
|
|
|
@@ -154,6 +162,7 @@ flash-prevention init script), **saves on every change**, and syncs across tabs
|
|
|
154
162
|
```
|
|
155
163
|
|
|
156
164
|
**3. Switch the visual style** by setting state — no DOM writes:
|
|
165
|
+
|
|
157
166
|
```svelte
|
|
158
167
|
<button onclick={() => (vibe.style = 'minimal')}>Minimal</button>
|
|
159
168
|
```
|
|
@@ -221,17 +230,19 @@ emits no utility (used inside `box-shadow` expressions).
|
|
|
221
230
|
Named tokens emit `--{token}` vars holding **complete color values** —
|
|
222
231
|
`--paper: oklch(0.985 0.005 85)` (oklch) or `--primary: rgb(232, 85, 43)` (rgb). Use them
|
|
223
232
|
directly in CSS-only contexts; for alpha, use `color-mix` (the form the preset itself uses):
|
|
233
|
+
|
|
224
234
|
```css
|
|
225
235
|
.thing { background: var(--paper); border-color: var(--paper-edge); }
|
|
226
236
|
.thing-50pc { background: color-mix(in oklch, var(--primary) 50%, transparent); }
|
|
227
237
|
```
|
|
238
|
+
|
|
228
239
|
Prefer utility classes over raw vars where you can.
|
|
229
240
|
|
|
230
241
|
---
|
|
231
242
|
|
|
232
243
|
## Layer 3b — Extending the vocabulary (`overrides` + `tokens: extended`)
|
|
233
244
|
|
|
234
|
-
**The named-token set is the
|
|
245
|
+
**The named-token set is the *default* vocabulary, not a hard limit.** When a design needs
|
|
235
246
|
a token the core set lacks — a second accent tone, a dedicated `on-accent` text color, a
|
|
236
247
|
divider-line color — add it. Don't conclude "core only has `accent`/`accent-soft`, so this
|
|
237
248
|
can't be expressed." Two mechanisms:
|
|
@@ -299,6 +310,7 @@ Default stays `'core'`.
|
|
|
299
310
|
|
|
300
311
|
**`rokkit init`** scaffolds `rokkit.config.js` in the named-token shape (a header comment
|
|
301
312
|
documents the vocabulary). Two starters via the palette prompt:
|
|
313
|
+
|
|
302
314
|
- **Tailwind (rgb)** — `skin` of Tailwind palette names, `colorSpace: 'rgb'`, `tokens: 'core'`. Single-palette → light-leaning (see Layer 2b).
|
|
303
315
|
- **Zen-Sumi (OKLCH)** — ships `palettes` (kami/sumi/shu/…), `colorSpace: 'oklch'`, dual-palette `surface`/`ink` → full dark mode out of the box.
|
|
304
316
|
|
|
@@ -308,7 +320,7 @@ mode init script), it runs **advisory** config-shape warnings and migration hint
|
|
|
308
320
|
fail the command or block `--fix`:
|
|
309
321
|
|
|
310
322
|
| Check | Warns when |
|
|
311
|
-
|
|
323
|
+
| ------- | ----------- |
|
|
312
324
|
| `skin-ink-role` | the active colormap has no `ink` role (text tokens fall back) |
|
|
313
325
|
| `oklch-needs-palettes` | `colorSpace: 'oklch'` but no `palettes` block |
|
|
314
326
|
| `colors-alias` | config uses the legacy `colors:` instead of `skin:` |
|
|
@@ -321,6 +333,7 @@ colors`. Run `rokkit doctor --fix` to auto-write a real starter config when one
|
|
|
321
333
|
## Typography configuration
|
|
322
334
|
|
|
323
335
|
Set faces in config; the preset emits CSS vars and Tailwind `fontFamily` keys.
|
|
336
|
+
|
|
324
337
|
```js
|
|
325
338
|
typography: {
|
|
326
339
|
display: "'Fraunces', 'Iowan Old Style', Georgia, serif", // heading/display face
|
|
@@ -328,8 +341,10 @@ typography: {
|
|
|
328
341
|
mono: "'JetBrains Mono', 'SF Mono', Menlo, monospace",
|
|
329
342
|
}
|
|
330
343
|
```
|
|
344
|
+
|
|
331
345
|
Canonical config keys are `display` / `ui` / `mono`; `heading` / `sans` are accepted
|
|
332
346
|
aliases. Usage classes: `font-heading` (display face), `font-body` (UI face), `font-mono`.
|
|
347
|
+
|
|
333
348
|
```html
|
|
334
349
|
<h1 class="font-heading text-2xl text-ink">Page Title</h1>
|
|
335
350
|
<p class="font-body text-[13px] text-ink-mute">Description</p>
|
|
@@ -343,7 +358,7 @@ aliases. Usage classes: `font-heading` (display face), `font-body` (UI face), `f
|
|
|
343
358
|
`shape: { radius: 'soft' }` selects a preset emitting `--radius-{sm,md,lg,xl,full}`.
|
|
344
359
|
|
|
345
360
|
| Preset | sm | md | lg | xl | full | Personality |
|
|
346
|
-
|
|
361
|
+
| -------- | ---- | ---- | ---- | ---- | ------ | ------------- |
|
|
347
362
|
| `sharp` | 0 | 0 | 0 | 0 | 9999px | Minimal, editorial |
|
|
348
363
|
| `soft` | 2px | 6px | 10px | 12px | 9999px | Professional, calm |
|
|
349
364
|
| `rounded` | 4px | 8px | 12px | 16px | 9999px | Friendly, modern |
|
|
@@ -360,7 +375,7 @@ never `rounded-[6px]`, which bypasses the config.
|
|
|
360
375
|
Rokkit inherits Tailwind's 4px-grid spacing. **Stay on the 4px grid, use Tailwind names.**
|
|
361
376
|
|
|
362
377
|
| Tailwind | px | When |
|
|
363
|
-
|
|
378
|
+
| ---------- | ---- | ------ |
|
|
364
379
|
| `1` / `2` / `3` | 4 / 8 / 12 | tight gap · component gap · compact padding |
|
|
365
380
|
| `4` / `6` | 16 / 24 | default padding · panel padding |
|
|
366
381
|
| `8` / `12` / `16` | 32 / 48 / 64 | section separation · page padding · hero whitespace |
|
|
@@ -400,7 +415,7 @@ export default {
|
|
|
400
415
|
## Common mistakes
|
|
401
416
|
|
|
402
417
|
| Mistake | Why it fails | Fix |
|
|
403
|
-
|
|
418
|
+
| --- | --- | --- |
|
|
404
419
|
| "core only emits `accent`/`accent-soft`, so I can't have `accent-2`/`on-accent`" | the vocabulary is extensible, not fixed | add them under `overrides:` (or use `tokens: 'extended'`) — see Layer 3b |
|
|
405
420
|
| Override palette ref `'sky-600'` (hyphen) | refs use **dot** notation | `'sky.600'` (and `sky` must be in `palettes:`) |
|
|
406
421
|
| `color: #3D3730` in a component | breaks on theme change | `text-ink` (or the right named token) |
|
|
@@ -14,7 +14,7 @@ shifts.
|
|
|
14
14
|
**Skin vs style vs mode:**
|
|
15
15
|
|
|
16
16
|
| Dimension | Controls | Example values |
|
|
17
|
-
|
|
17
|
+
| ----------- | ---------- | ---------------- |
|
|
18
18
|
| **skin** | which palette backs the roles | `default` · `ocean` · `violet` |
|
|
19
19
|
| **style** | which visual theme (border radius, shadow, surface recipe) | `rokkit` · `minimal` · `zen-sumi` |
|
|
20
20
|
| **mode** | light or dark rendering | `light` · `dark` |
|
|
@@ -59,7 +59,7 @@ The preset ships five built-in skins in `@rokkit/unocss`. Their `[data-skin='nam
|
|
|
59
59
|
blocks are emitted automatically — no config needed to use them:
|
|
60
60
|
|
|
61
61
|
| Skin | Surface | Ink | Primary | Accent |
|
|
62
|
-
|
|
62
|
+
| ------ | --------- | ----- | --------- | -------- |
|
|
63
63
|
| `default` | (DEFAULT_THEME_MAPPING) | `slate` | — | — |
|
|
64
64
|
| `ocean` | `slate` | `slate` | `sky` | `teal` |
|
|
65
65
|
| `violet` | `zinc` | `zinc` | `violet` | `indigo` |
|
|
@@ -124,7 +124,7 @@ When `storageKey` is provided it also loads/saves the persisted skin.
|
|
|
124
124
|
**`SkinSwitcherToggle` props:**
|
|
125
125
|
|
|
126
126
|
| Prop | Type | Default | Purpose |
|
|
127
|
-
|
|
127
|
+
| ------ | ------ | --------- | --------- |
|
|
128
128
|
| `skins` | `Array<string \| { name, label?, icon? }>` | `vibe.allowedSkins` | skin options |
|
|
129
129
|
| `labels` | `Record<string, string>` | `{}` | display labels keyed by name |
|
|
130
130
|
| `showLabels` | `boolean` | `false` | show label text in the toggle |
|
|
@@ -215,7 +215,7 @@ runtime-only color overrides.
|
|
|
215
215
|
## Common mistakes
|
|
216
216
|
|
|
217
217
|
| Mistake | Why it fails | Fix |
|
|
218
|
-
|
|
218
|
+
| --------- | ------------- | ----- |
|
|
219
219
|
| `vibe.allowedSkins` set after `themable` runs | persisted skin can't restore; `vibe.skin = storedValue` is a no-op because the list defaults to `['default']` only | set `vibe.allowedSkins` before `<svelte:body use:themable ...>` in the root layout |
|
|
220
220
|
| `class="skin-ocean"` on an element | `skin-{name}` class was removed | use `data-skin='ocean'` attribute, or let `themable` write it via `vibe.skin` |
|
|
221
221
|
| Hand-rolling `<style>` injection for a skin | brittle, order-dependent, bypasses named-token aliases | define in `rokkit.config.js` `skins:` + let the preset emit `[data-skin='name']` blocks |
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import {
|
|
3
|
+
readFileSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
copyFileSync
|
|
9
|
+
} from 'node:fs'
|
|
10
|
+
import { join, resolve } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
import prompts from 'prompts'
|
|
13
|
+
import { parseFrontmatter } from './skills.js'
|
|
14
|
+
|
|
15
|
+
// The catalog ships inside the package: packages/cli/agents/ in dev/test,
|
|
16
|
+
// node_modules/@rokkit/cli/agents/ once installed. Agents are single .md files
|
|
17
|
+
// (unlike skills, which are directories with a SKILL.md).
|
|
18
|
+
// Guard against non-file: URLs (e.g. jsdom sets import.meta.url to http://localhost/...).
|
|
19
|
+
const DEFAULT_AGENTS_DIR = (() => {
|
|
20
|
+
try {
|
|
21
|
+
return fileURLToPath(new URL('../agents/', import.meta.url))
|
|
22
|
+
} catch {
|
|
23
|
+
return resolve(process.cwd(), 'packages/cli/agents')
|
|
24
|
+
}
|
|
25
|
+
})()
|
|
26
|
+
|
|
27
|
+
// Public base the CLI can pull agents from when the bundled copy is unavailable
|
|
28
|
+
// (`--remote`). Same corpus that the sensei.library.json manifest advertises.
|
|
29
|
+
const DEFAULT_AGENTS_URL = 'https://rokkit.sensei-hq.com/agents'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read the bundled agent catalog (derived from each <name>.md frontmatter).
|
|
33
|
+
* @param {{ agentsDir?: string }} [opts]
|
|
34
|
+
* @returns {Array<{ name: string, description: string }>}
|
|
35
|
+
*/
|
|
36
|
+
export function listAgents({ agentsDir = DEFAULT_AGENTS_DIR } = {}) {
|
|
37
|
+
if (!existsSync(agentsDir)) return []
|
|
38
|
+
return readdirSync(agentsDir)
|
|
39
|
+
.filter((entry) => entry.endsWith('.md'))
|
|
40
|
+
.map((entry) => parseFrontmatter(readFileSync(join(agentsDir, entry), 'utf-8')))
|
|
41
|
+
.filter((a) => a.name)
|
|
42
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Copy named agents into <cwd>/.claude/agents/<name>.md from the bundled catalog (idempotent).
|
|
47
|
+
* @param {string[]} names
|
|
48
|
+
* @param {{ cwd?: string, force?: boolean, agentsDir?: string }} [opts]
|
|
49
|
+
* @returns {Array<{ name: string, status: 'added' | 'skipped' | 'unknown' }>}
|
|
50
|
+
*/
|
|
51
|
+
export function installAgents(
|
|
52
|
+
names,
|
|
53
|
+
{ cwd = process.cwd(), force = false, agentsDir = DEFAULT_AGENTS_DIR } = {}
|
|
54
|
+
) {
|
|
55
|
+
const targetRoot = resolve(cwd, '.claude', 'agents')
|
|
56
|
+
return names.map((name) => {
|
|
57
|
+
const src = join(agentsDir, `${name}.md`)
|
|
58
|
+
if (!existsSync(src)) return { name, status: 'unknown' }
|
|
59
|
+
const dest = join(targetRoot, `${name}.md`)
|
|
60
|
+
if (existsSync(dest) && !force) return { name, status: 'skipped' }
|
|
61
|
+
mkdirSync(targetRoot, { recursive: true })
|
|
62
|
+
copyFileSync(src, dest)
|
|
63
|
+
return { name, status: 'added' }
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Fetch named agents from the public site and write them into <cwd>/.claude/agents/<name>.md.
|
|
69
|
+
* Lets the CLI pull from the site when the bundled catalog is absent (or on `--remote`).
|
|
70
|
+
* @param {string[]} names
|
|
71
|
+
* @param {{ cwd?: string, force?: boolean, baseUrl?: string, fetchImpl?: typeof fetch }} [opts]
|
|
72
|
+
* @returns {Promise<Array<{ name: string, status: 'added' | 'skipped' | 'unknown' }>>}
|
|
73
|
+
*/
|
|
74
|
+
export async function fetchAgents(
|
|
75
|
+
names,
|
|
76
|
+
{ cwd = process.cwd(), force = false, baseUrl = DEFAULT_AGENTS_URL, fetchImpl = fetch } = {}
|
|
77
|
+
) {
|
|
78
|
+
const targetRoot = resolve(cwd, '.claude', 'agents')
|
|
79
|
+
const results = []
|
|
80
|
+
for (const name of names) {
|
|
81
|
+
const dest = join(targetRoot, `${name}.md`)
|
|
82
|
+
if (existsSync(dest) && !force) {
|
|
83
|
+
results.push({ name, status: 'skipped' })
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
const res = await fetchImpl(`${baseUrl}/${name}.md`)
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
results.push({ name, status: 'unknown' })
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
mkdirSync(targetRoot, { recursive: true })
|
|
92
|
+
writeFileSync(dest, await res.text())
|
|
93
|
+
results.push({ name, status: 'added' })
|
|
94
|
+
}
|
|
95
|
+
return results
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Print the catalog, marking already-installed agents.
|
|
100
|
+
* @param {{ agentsDir?: string, cwd?: string }} [opts]
|
|
101
|
+
*/
|
|
102
|
+
export async function runAgentsList({ agentsDir, cwd = process.cwd() } = {}) {
|
|
103
|
+
const catalog = listAgents({ agentsDir })
|
|
104
|
+
if (catalog.length === 0) {
|
|
105
|
+
console.info('No agents available.')
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
const installedRoot = resolve(cwd, '.claude', 'agents')
|
|
109
|
+
console.info('Available Rokkit agents:\n')
|
|
110
|
+
for (const a of catalog) {
|
|
111
|
+
const mark = existsSync(join(installedRoot, `${a.name}.md`)) ? '✓ ' : ' '
|
|
112
|
+
console.info(`${mark}${a.name}`)
|
|
113
|
+
console.info(` ${a.description}`)
|
|
114
|
+
}
|
|
115
|
+
console.info('\nInstall with: rokkit agents add <name> [...] (or --all)')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve which agents to install, then copy (or fetch with --remote) and report.
|
|
120
|
+
* @param {string[]} names
|
|
121
|
+
* @param {{ all?: boolean, force?: boolean, remote?: boolean, agentsDir?: string, cwd?: string, fetchImpl?: typeof fetch }} [opts]
|
|
122
|
+
*/
|
|
123
|
+
export async function runAgentsAdd(
|
|
124
|
+
names,
|
|
125
|
+
{ all = false, force = false, remote = false, agentsDir, cwd = process.cwd(), fetchImpl } = {}
|
|
126
|
+
) {
|
|
127
|
+
const catalog = listAgents({ agentsDir })
|
|
128
|
+
let selected = names
|
|
129
|
+
if (all) {
|
|
130
|
+
selected = catalog.map((a) => a.name)
|
|
131
|
+
} else if (selected.length === 0) {
|
|
132
|
+
const res = await prompts({
|
|
133
|
+
type: 'multiselect',
|
|
134
|
+
name: 'picked',
|
|
135
|
+
message: 'Select agents to add',
|
|
136
|
+
choices: catalog.map((a) => ({ title: a.name, value: a.name, description: a.description }))
|
|
137
|
+
})
|
|
138
|
+
selected = res.picked ?? []
|
|
139
|
+
}
|
|
140
|
+
if (selected.length === 0) {
|
|
141
|
+
console.info('No agents selected.')
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const results = remote
|
|
145
|
+
? await fetchAgents(selected, { cwd, force, fetchImpl })
|
|
146
|
+
: installAgents(selected, { cwd, force, agentsDir })
|
|
147
|
+
for (const r of results) {
|
|
148
|
+
if (r.status === 'added') console.info(` added .claude/agents/${r.name}.md`)
|
|
149
|
+
else if (r.status === 'skipped')
|
|
150
|
+
console.info(` skipped ${r.name} (exists — use --force to overwrite)`)
|
|
151
|
+
else console.error(` unknown agent: ${r.name}`)
|
|
152
|
+
}
|
|
153
|
+
if (results.some((r) => r.status === 'unknown')) {
|
|
154
|
+
if (!remote) console.error(`\nValid agents: ${catalog.map((a) => a.name).join(', ')}`)
|
|
155
|
+
process.exitCode = 1
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* CLI entry for `rokkit agents` subcommands.
|
|
161
|
+
* @param {'list' | 'add'} sub
|
|
162
|
+
* @param {{ _?: string[], all?: boolean, force?: boolean, remote?: boolean }} [opts]
|
|
163
|
+
*/
|
|
164
|
+
export async function agentsCommand(sub, opts = {}) {
|
|
165
|
+
if (sub === 'add') {
|
|
166
|
+
await runAgentsAdd(opts._ ?? [], { all: opts.all, force: opts.force, remote: opts.remote })
|
|
167
|
+
} else {
|
|
168
|
+
await runAgentsList()
|
|
169
|
+
}
|
|
170
|
+
}
|
package/src/index.js
CHANGED
|
@@ -139,4 +139,23 @@ prog
|
|
|
139
139
|
await skillsCommand('add', opts)
|
|
140
140
|
})
|
|
141
141
|
|
|
142
|
+
prog
|
|
143
|
+
.command('agents list')
|
|
144
|
+
.describe('List available Rokkit review agents')
|
|
145
|
+
.action(async () => {
|
|
146
|
+
const { agentsCommand } = await import('./agents.js')
|
|
147
|
+
await agentsCommand('list')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
prog
|
|
151
|
+
.command('agents add')
|
|
152
|
+
.describe('Add Rokkit review agents to .claude/agents/')
|
|
153
|
+
.option('--all', 'Install all available agents')
|
|
154
|
+
.option('--force', 'Overwrite an existing agent')
|
|
155
|
+
.option('--remote', 'Pull agents from the site instead of the bundled copy')
|
|
156
|
+
.action(async (opts) => {
|
|
157
|
+
const { agentsCommand } = await import('./agents.js')
|
|
158
|
+
await agentsCommand('add', opts)
|
|
159
|
+
})
|
|
160
|
+
|
|
142
161
|
prog.parse(process.argv)
|