@rokkit/cli 1.3.15 → 1.4.1
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/dist/src/agents.d.ts +2 -2
- package/dist/src/init.d.ts +402 -17
- package/dist/src/skills.d.ts +1 -1
- package/dist/src/validate.d.ts +8 -27
- package/package.json +2 -2
- package/src/agents.js +73 -40
- package/src/contrast.js +141 -82
- package/src/doctor.js +29 -36
- package/src/init.js +126 -87
- package/src/skills.js +51 -25
- package/src/validate.js +59 -41
package/src/skills.js
CHANGED
|
@@ -24,12 +24,26 @@ export function parseFrontmatter(md) {
|
|
|
24
24
|
// Keep these as literal regexes — do NOT collapse into a dynamic
|
|
25
25
|
// `new RegExp(`^${key}:...`)` helper: the keys are fixed, so the dynamic form
|
|
26
26
|
// adds nothing but trips the no-dynamic-regex lint, and the literals are
|
|
27
|
-
// plainly linear-time.
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
return {
|
|
27
|
+
// plainly linear-time.
|
|
28
|
+
//
|
|
29
|
+
// The block is captured UNTRIMMED on purpose: the field regexes are anchored
|
|
30
|
+
// with `^`, so indented frontmatter should miss, exactly as before.
|
|
31
|
+
const block = capture(md, /^---\n([\s\S]*?)\n---/)
|
|
32
|
+
return {
|
|
33
|
+
name: capture(block, /^name:\s*(.+)$/m).trim(),
|
|
34
|
+
description: capture(block, /^description:\s*(.+)$/m).trim()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* First capture group of `re` in `text`, or `''` when there is no match — so a
|
|
40
|
+
* missing block, a missing field and a bare `key:` all collapse to `''`.
|
|
41
|
+
* @param {string} text
|
|
42
|
+
* @param {RegExp} re
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
function capture(text, re) {
|
|
46
|
+
return text.match(re)?.[1] ?? ''
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
/**
|
|
@@ -72,7 +86,7 @@ export function installSkills(
|
|
|
72
86
|
* Print the catalog, marking already-installed skills.
|
|
73
87
|
* @param {{ skillsDir?: string, cwd?: string }} [opts]
|
|
74
88
|
*/
|
|
75
|
-
export
|
|
89
|
+
export function runSkillsList({ skillsDir, cwd = process.cwd() } = {}) {
|
|
76
90
|
const catalog = listSkills({ skillsDir })
|
|
77
91
|
if (catalog.length === 0) {
|
|
78
92
|
console.info('No skills available.')
|
|
@@ -98,35 +112,47 @@ export async function runSkillsAdd(
|
|
|
98
112
|
{ all = false, force = false, skillsDir, cwd = process.cwd() } = {}
|
|
99
113
|
) {
|
|
100
114
|
const catalog = listSkills({ skillsDir })
|
|
101
|
-
|
|
102
|
-
if (all) {
|
|
103
|
-
selected = catalog.map((s) => s.name)
|
|
104
|
-
} else if (selected.length === 0) {
|
|
105
|
-
const res = await prompts({
|
|
106
|
-
type: 'multiselect',
|
|
107
|
-
name: 'picked',
|
|
108
|
-
message: 'Select skills to add',
|
|
109
|
-
choices: catalog.map((s) => ({ title: s.name, value: s.name, description: s.description }))
|
|
110
|
-
})
|
|
111
|
-
selected = res.picked ?? []
|
|
112
|
-
}
|
|
115
|
+
const selected = await resolveSkillSelection(names, catalog, all)
|
|
113
116
|
if (selected.length === 0) {
|
|
114
117
|
console.info('No skills selected.')
|
|
115
118
|
return
|
|
116
119
|
}
|
|
117
120
|
const results = installSkills(selected, { cwd, force, skillsDir })
|
|
118
|
-
|
|
119
|
-
if (r.status === 'added') console.info(` added .claude/skills/${r.name}`)
|
|
120
|
-
else if (r.status === 'skipped')
|
|
121
|
-
console.info(` skipped ${r.name} (exists — use --force to overwrite)`)
|
|
122
|
-
else console.error(` unknown skill: ${r.name}`)
|
|
123
|
-
}
|
|
121
|
+
results.forEach(reportSkillResult)
|
|
124
122
|
if (results.some((r) => r.status === 'unknown')) {
|
|
125
123
|
console.error(`\nValid skills: ${catalog.map((s) => s.name).join(', ')}`)
|
|
126
124
|
process.exitCode = 1
|
|
127
125
|
}
|
|
128
126
|
}
|
|
129
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Which skills to install: everything with `--all`, the named ones, or an
|
|
130
|
+
* interactive pick when neither was given.
|
|
131
|
+
* @param {string[]} names
|
|
132
|
+
* @param {Array<{ name: string, description: string }>} catalog
|
|
133
|
+
* @param {boolean} all
|
|
134
|
+
* @returns {Promise<string[]>}
|
|
135
|
+
*/
|
|
136
|
+
async function resolveSkillSelection(names, catalog, all) {
|
|
137
|
+
if (all) return catalog.map((s) => s.name)
|
|
138
|
+
if (names.length > 0) return names
|
|
139
|
+
const res = await prompts({
|
|
140
|
+
type: 'multiselect',
|
|
141
|
+
name: 'picked',
|
|
142
|
+
message: 'Select skills to add',
|
|
143
|
+
choices: catalog.map((s) => ({ title: s.name, value: s.name, description: s.description }))
|
|
144
|
+
})
|
|
145
|
+
return res.picked ?? []
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Report one install outcome. Unknown names go to stderr — they're a user error. */
|
|
149
|
+
function reportSkillResult(r) {
|
|
150
|
+
if (r.status === 'added') console.info(` added .claude/skills/${r.name}`)
|
|
151
|
+
else if (r.status === 'skipped')
|
|
152
|
+
console.info(` skipped ${r.name} (exists — use --force to overwrite)`)
|
|
153
|
+
else console.error(` unknown skill: ${r.name}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
/**
|
|
131
157
|
* CLI entry for `rokkit skills` subcommands.
|
|
132
158
|
* @param {'list' | 'add'} sub
|
package/src/validate.js
CHANGED
|
@@ -8,41 +8,48 @@
|
|
|
8
8
|
* @param {Record<string, unknown> | null} config
|
|
9
9
|
* @returns {Array<{ id: string, label: string, status: 'warn', fixable: false, fix: string }>}
|
|
10
10
|
*/
|
|
11
|
+
/** True when the config still uses the back-compat `colors:` key instead of `skin:`. */
|
|
12
|
+
function usesLegacyColorsAlias({ colors, skin }) {
|
|
13
|
+
return typeof colors === 'object' && colors !== null && !Array.isArray(colors) && !skin
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** True when oklch values are requested but no `palettes` block supplies them. */
|
|
17
|
+
function oklchWithoutPalettes(config) {
|
|
18
|
+
return config.colorSpace === 'oklch' && Object.keys(config.palettes ?? {}).length === 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The config-shape rules, in report order: a predicate plus the advisory it
|
|
23
|
+
* emits. Adding a rule is a new entry rather than another `if` block, and each
|
|
24
|
+
* predicate stays small enough to read on its own.
|
|
25
|
+
*/
|
|
26
|
+
const SHAPE_RULES = [
|
|
27
|
+
{
|
|
28
|
+
when: ({ colormap }) => !('ink' in colormap),
|
|
29
|
+
id: 'skin-ink-role',
|
|
30
|
+
label: 'skin defines an `ink` role',
|
|
31
|
+
fix: "Add `ink: '<palette>'` (reusing the surface palette is fine) — without it, ink-* text tokens fall back to the surface palette."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
when: ({ config }) => oklchWithoutPalettes(config),
|
|
35
|
+
id: 'oklch-needs-palettes',
|
|
36
|
+
label: 'colorSpace `oklch` has a `palettes` block',
|
|
37
|
+
fix: 'oklch values need a `palettes` block of bare "L C H" components; Tailwind named colors are rgb.'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
when: ({ config }) => usesLegacyColorsAlias(config),
|
|
41
|
+
id: 'colors-alias',
|
|
42
|
+
label: 'config uses `skin` (not the legacy `colors` alias)',
|
|
43
|
+
fix: 'Rename `colors:` to `skin:` — `colors` is a back-compat alias.'
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
|
|
11
47
|
export function validateConfigShape(config) {
|
|
12
48
|
if (!config) return []
|
|
13
|
-
const checks = []
|
|
14
|
-
const usesColorsAlias =
|
|
15
|
-
typeof config.colors === 'object' && config.colors !== null && !Array.isArray(config.colors) && !config.skin
|
|
16
49
|
const colormap = config.skins?.default ?? config.skin ?? config.colors ?? {}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
id: 'skin-ink-role',
|
|
21
|
-
label: 'skin defines an `ink` role',
|
|
22
|
-
status: 'warn',
|
|
23
|
-
fixable: false,
|
|
24
|
-
fix: "Add `ink: '<palette>'` (reusing the surface palette is fine) — without it, ink-* text tokens fall back to the surface palette."
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
if (config.colorSpace === 'oklch' && Object.keys(config.palettes ?? {}).length === 0) {
|
|
28
|
-
checks.push({
|
|
29
|
-
id: 'oklch-needs-palettes',
|
|
30
|
-
label: 'colorSpace `oklch` has a `palettes` block',
|
|
31
|
-
status: 'warn',
|
|
32
|
-
fixable: false,
|
|
33
|
-
fix: 'oklch values need a `palettes` block of bare "L C H" components; Tailwind named colors are rgb.'
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
if (usesColorsAlias) {
|
|
37
|
-
checks.push({
|
|
38
|
-
id: 'colors-alias',
|
|
39
|
-
label: 'config uses `skin` (not the legacy `colors` alias)',
|
|
40
|
-
status: 'warn',
|
|
41
|
-
fixable: false,
|
|
42
|
-
fix: 'Rename `colors:` to `skin:` — `colors` is a back-compat alias.'
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
return checks
|
|
50
|
+
return SHAPE_RULES.filter((rule) => rule.when({ config, colormap })).map(
|
|
51
|
+
({ id, label, fix }) => ({ id, label, status: 'warn', fixable: false, fix })
|
|
52
|
+
)
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
/**
|
|
@@ -62,19 +69,30 @@ const TEXT_MISUSE_PATTERNS = [
|
|
|
62
69
|
* @param {{ list?: (dir: string) => string[], exists: (p: string) => boolean, read: (p: string) => string, resolve: (p: string) => string }} fs
|
|
63
70
|
* @returns {Array<{ id: string, label: string, status: 'warn', fixable: false, fix: string }>}
|
|
64
71
|
*/
|
|
72
|
+
/**
|
|
73
|
+
* `path:line` for every line of `source` that misuses a surface token as text
|
|
74
|
+
* colour. Paths are reported relative to the cwd when they sit under it.
|
|
75
|
+
* @param {string} file
|
|
76
|
+
* @param {string} source
|
|
77
|
+
* @param {string} cwd
|
|
78
|
+
* @returns {string[]}
|
|
79
|
+
*/
|
|
80
|
+
function misuseHits(file, source, cwd) {
|
|
81
|
+
const display = file.startsWith(cwd) ? file.slice(cwd.length).replace(/^\//, '') : file
|
|
82
|
+
return source
|
|
83
|
+
.split('\n')
|
|
84
|
+
.map((line, i) => (TEXT_MISUSE_PATTERNS.some((re) => re.test(line)) ? `${display}:${i + 1}` : null))
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
}
|
|
87
|
+
|
|
65
88
|
export function checkTextTokenUsage(fs) {
|
|
66
89
|
if (typeof fs.list !== 'function' || !fs.exists(fs.resolve('src'))) return []
|
|
67
90
|
|
|
68
91
|
const cwd = process.cwd()
|
|
69
|
-
const hits =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const display = file.startsWith(cwd) ? file.slice(cwd.length).replace(/^\//, '') : file
|
|
74
|
-
lines.forEach((line, i) => {
|
|
75
|
-
if (TEXT_MISUSE_PATTERNS.some((re) => re.test(line))) hits.push(`${display}:${i + 1}`)
|
|
76
|
-
})
|
|
77
|
-
}
|
|
92
|
+
const hits = fs
|
|
93
|
+
.list(fs.resolve('src'))
|
|
94
|
+
.filter((file) => file.endsWith('.css') || file.endsWith('.svelte'))
|
|
95
|
+
.flatMap((file) => misuseHits(file, fs.read(file), cwd))
|
|
78
96
|
|
|
79
97
|
if (hits.length === 0) return []
|
|
80
98
|
return [
|