@rokkit/cli 1.0.0-next.127 → 1.0.0-next.128

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,24 @@
1
+ /**
2
+ * Run all doctor checks against the given filesystem adapter.
3
+ * @param {{ exists: (p: string) => boolean, read: (p: string) => string, resolve: (p: string) => string }} fs
4
+ * @returns {Array<{ id: string, label: string, status: 'pass'|'fail', fixable: boolean, fix: string, autoFix?: string }>}
5
+ */
6
+ export function runChecks(fs: {
7
+ exists: (p: string) => boolean;
8
+ read: (p: string) => string;
9
+ resolve: (p: string) => string;
10
+ }): Array<{
11
+ id: string;
12
+ label: string;
13
+ status: "pass" | "fail";
14
+ fixable: boolean;
15
+ fix: string;
16
+ autoFix?: string;
17
+ }>;
18
+ /**
19
+ * Interactive doctor command — validates project setup.
20
+ * @param {{ fix?: boolean }} [opts]
21
+ */
22
+ export function doctor(opts?: {
23
+ fix?: boolean;
24
+ }): Promise<void>;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Generate a Rokkit config object from user choices.
3
+ * @param {{ palette: string, customColors?: Record<string, string>, icons: string, iconPath?: string, themes: string[], switcher: string }} opts
4
+ * @returns {Record<string, unknown>}
5
+ */
6
+ export function generateConfig({ palette, customColors, icons, iconPath, themes, switcher }: {
7
+ palette: string;
8
+ customColors?: Record<string, string>;
9
+ icons: string;
10
+ iconPath?: string;
11
+ themes: string[];
12
+ switcher: string;
13
+ }): Record<string, unknown>;
14
+ /**
15
+ * Generate the content for uno.config.js.
16
+ * @returns {string}
17
+ */
18
+ export function generateUnoConfig(): string;
19
+ /**
20
+ * Generate an array of CSS @import lines for app.css.
21
+ * @param {string[]} themes
22
+ * @returns {string[]}
23
+ */
24
+ export function generateAppCssImports(themes: string[]): string[];
25
+ /**
26
+ * Generate the flash-prevention script for app.html.
27
+ * Returns null when no script is needed (system switcher).
28
+ * @param {string} switcher — 'system' | 'manual' | 'full'
29
+ * @param {string} [storageKey='rokkit-theme']
30
+ * @returns {string | null}
31
+ */
32
+ export function generateInitScript(switcher: string, storageKey?: string): string | null;
33
+ /**
34
+ * Interactive init command — prompts the user, writes config files.
35
+ */
36
+ export function init(): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/cli",
3
- "version": "1.0.0-next.127",
3
+ "version": "1.0.0-next.128",
4
4
  "description": "Command line utilities for icon bundling and conversion.",
5
5
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
6
  "license": "MIT",
@@ -25,6 +25,7 @@
25
25
  "sade": "^1.8.1",
26
26
  "@iconify/tools": "5.0.3",
27
27
  "@iconify/types": "^2.0.0",
28
+ "prompts": "^2.4.2",
28
29
  "ramda": "^0.32.0"
29
30
  },
30
31
  "files": [
package/src/doctor.js ADDED
@@ -0,0 +1,186 @@
1
+ /* eslint-disable no-console */
2
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
3
+ import { resolve } from 'path'
4
+ import { generateUnoConfig, generateAppCssImports, generateInitScript } from './init.js'
5
+
6
+ /**
7
+ * Run all doctor checks against the given filesystem adapter.
8
+ * @param {{ exists: (p: string) => boolean, read: (p: string) => string, resolve: (p: string) => string }} fs
9
+ * @returns {Array<{ id: string, label: string, status: 'pass'|'fail', fixable: boolean, fix: string, autoFix?: string }>}
10
+ */
11
+ export function runChecks(fs) {
12
+ const checks = []
13
+
14
+ // 1. rokkit.config.js exists
15
+ const configExists = fs.exists(fs.resolve('rokkit.config.js'))
16
+ checks.push({
17
+ id: 'config-exists',
18
+ label: 'rokkit.config.js exists',
19
+ status: configExists ? 'pass' : 'fail',
20
+ fixable: true,
21
+ fix: 'Run `rokkit init` to generate config',
22
+ autoFix: 'generate-config'
23
+ })
24
+
25
+ // 2. uno.config.js uses presetRokkit
26
+ const unoPath = fs.resolve('uno.config.js')
27
+ const unoExists = fs.exists(unoPath)
28
+ let unoUsesPreset = false
29
+ if (unoExists) {
30
+ const unoContent = fs.read(unoPath)
31
+ unoUsesPreset = unoContent.includes('presetRokkit')
32
+ }
33
+ checks.push({
34
+ id: 'uno-uses-preset',
35
+ label: 'uno.config.js uses presetRokkit()',
36
+ status: unoUsesPreset ? 'pass' : 'fail',
37
+ fixable: false,
38
+ fix: unoExists
39
+ ? `Replace uno.config.js contents with:\n${ generateUnoConfig()}`
40
+ : `Create uno.config.js with:\n${ generateUnoConfig()}`
41
+ })
42
+
43
+ // 3. app.css has theme imports
44
+ const cssPath = fs.resolve('src/app.css')
45
+ const cssExists = fs.exists(cssPath)
46
+ let cssHasBase = false
47
+ if (cssExists) {
48
+ const css = fs.read(cssPath)
49
+ cssHasBase = css.includes('@rokkit/themes/dist/base')
50
+ }
51
+ checks.push({
52
+ id: 'css-imports',
53
+ label: 'app.css has theme imports',
54
+ status: cssHasBase ? 'pass' : 'fail',
55
+ fixable: true,
56
+ fix: 'Append theme imports to src/app.css',
57
+ autoFix: 'patch-css'
58
+ })
59
+
60
+ // 4. app.html has init script
61
+ const htmlPath = fs.resolve('src/app.html')
62
+ const htmlExists = fs.exists(htmlPath)
63
+ let htmlHasScript = false
64
+ if (htmlExists) {
65
+ const html = fs.read(htmlPath)
66
+ htmlHasScript = html.includes('rokkit-theme') || html.includes('data-mode')
67
+ }
68
+ checks.push({
69
+ id: 'html-init-script',
70
+ label: 'app.html has theme init script',
71
+ status: htmlHasScript ? 'pass' : 'fail',
72
+ fixable: true,
73
+ fix: 'Add flash-prevention script to src/app.html',
74
+ autoFix: 'patch-html'
75
+ })
76
+
77
+ return checks
78
+ }
79
+
80
+ /**
81
+ * Create a real filesystem adapter rooted at cwd.
82
+ * @param {string} cwd
83
+ */
84
+ function createFsAdapter(cwd) {
85
+ return {
86
+ exists: (p) => existsSync(p),
87
+ read: (p) => readFileSync(p, 'utf-8'),
88
+ resolve: (p) => resolve(cwd, p)
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Auto-fix failed checks that are marked fixable.
94
+ * @param {Array<{ id: string, status: string, fixable: boolean, autoFix?: string }>} checks
95
+ * @param {string} cwd
96
+ * @returns {number} number of fixed items
97
+ */
98
+ function autoFix(checks, cwd) {
99
+ let fixed = 0
100
+
101
+ for (const check of checks) {
102
+ if (check.status !== 'fail' || !check.fixable) continue
103
+
104
+ if (check.autoFix === 'generate-config') {
105
+ const configPath = resolve(cwd, 'rokkit.config.js')
106
+ writeFileSync(configPath, 'export default {}\n')
107
+ console.info(` Fixed: ${check.label}`)
108
+ fixed++
109
+ }
110
+
111
+ if (check.autoFix === 'patch-css') {
112
+ const cssPath = resolve(cwd, 'src/app.css')
113
+ const imports = generateAppCssImports(['rokkit'])
114
+ if (existsSync(cssPath)) {
115
+ const existing = readFileSync(cssPath, 'utf-8')
116
+ const missing = imports.filter((line) => !existing.includes(line))
117
+ if (missing.length > 0) {
118
+ writeFileSync(cssPath, `${missing.join('\n') }\n${ existing}`)
119
+ }
120
+ } else {
121
+ writeFileSync(cssPath, `${imports.join('\n') }\n`)
122
+ }
123
+ console.info(` Fixed: ${check.label}`)
124
+ fixed++
125
+ }
126
+
127
+ if (check.autoFix === 'patch-html') {
128
+ const htmlPath = resolve(cwd, 'src/app.html')
129
+ if (existsSync(htmlPath)) {
130
+ const html = readFileSync(htmlPath, 'utf-8')
131
+ const script = generateInitScript('manual', 'rokkit-theme')
132
+ if (script && !html.includes('rokkit-theme')) {
133
+ const patched = html.replace(/(<body[^>]*>)/, `$1\n${script}`)
134
+ writeFileSync(htmlPath, patched)
135
+ console.info(` Fixed: ${check.label}`)
136
+ fixed++
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ return fixed
143
+ }
144
+
145
+ /**
146
+ * Interactive doctor command — validates project setup.
147
+ * @param {{ fix?: boolean }} [opts]
148
+ */
149
+ export async function doctor(opts = {}) {
150
+ const cwd = process.cwd()
151
+ const fs = createFsAdapter(cwd)
152
+ const checks = runChecks(fs)
153
+
154
+ console.info('Rokkit Doctor\n')
155
+
156
+ let failures = 0
157
+ for (const check of checks) {
158
+ const icon = check.status === 'pass' ? 'PASS' : 'FAIL'
159
+ console.info(` ${icon} ${check.label}`)
160
+ if (check.status === 'fail') {
161
+ failures++
162
+ if (!opts.fix) {
163
+ console.info(` ${check.fixable ? '(auto-fixable) ' : ''}${check.fix}`)
164
+ }
165
+ }
166
+ }
167
+
168
+ if (failures > 0 && opts.fix) {
169
+ console.info('\nAuto-fixing...\n')
170
+ const fixed = autoFix(checks, cwd)
171
+ const remaining = failures - fixed
172
+ if (remaining > 0) {
173
+ console.info(`\n${fixed} fixed, ${remaining} require manual action:`)
174
+ for (const check of checks) {
175
+ if (check.status === 'fail' && !check.fixable) {
176
+ console.info(` - ${check.label}: ${check.fix}`)
177
+ }
178
+ }
179
+ } else {
180
+ console.info(`\nAll ${fixed} issues fixed!`)
181
+ }
182
+ }
183
+
184
+ console.info('')
185
+ process.exitCode = failures > 0 && !opts.fix ? 1 : 0
186
+ }
package/src/index.js CHANGED
@@ -53,4 +53,21 @@ prog
53
53
  convertFolders(folders, config, opts)
54
54
  })
55
55
 
56
+ prog
57
+ .command('init')
58
+ .describe('Initialize Rokkit in an existing SvelteKit project')
59
+ .action(async (opts) => {
60
+ const { init } = await import('./init.js')
61
+ await init(opts)
62
+ })
63
+
64
+ prog
65
+ .command('doctor')
66
+ .describe('Validate Rokkit project setup')
67
+ .option('--fix', 'Auto-fix safe issues')
68
+ .action(async (opts) => {
69
+ const { doctor } = await import('./doctor.js')
70
+ await doctor(opts)
71
+ })
72
+
56
73
  prog.parse(process.argv)
package/src/init.js ADDED
@@ -0,0 +1,254 @@
1
+ /* eslint-disable no-console */
2
+ import prompts from 'prompts'
3
+ import { writeFileSync, readFileSync, existsSync } from 'fs'
4
+ import { resolve } from 'path'
5
+
6
+ const SKIN_PRESETS = {
7
+ default: { primary: 'orange', secondary: 'pink', accent: 'sky', surface: 'slate' },
8
+ vibrant: { primary: 'blue', secondary: 'purple', accent: 'sky', surface: 'slate' },
9
+ seaweed: {
10
+ primary: 'sky',
11
+ secondary: 'green',
12
+ accent: 'blue',
13
+ surface: 'zinc',
14
+ danger: 'rose',
15
+ error: 'rose',
16
+ success: 'lime',
17
+ warning: 'amber',
18
+ info: 'indigo'
19
+ }
20
+ }
21
+
22
+ const DEFAULT_COLORS = {
23
+ primary: 'orange',
24
+ secondary: 'pink',
25
+ accent: 'sky',
26
+ surface: 'slate',
27
+ success: 'green',
28
+ warning: 'yellow',
29
+ danger: 'red',
30
+ error: 'red',
31
+ info: 'cyan'
32
+ }
33
+
34
+ /**
35
+ * Generate a Rokkit config object from user choices.
36
+ * @param {{ palette: string, customColors?: Record<string, string>, icons: string, iconPath?: string, themes: string[], switcher: string }} opts
37
+ * @returns {Record<string, unknown>}
38
+ */
39
+ export function generateConfig({ palette, customColors, icons, iconPath, themes, switcher }) {
40
+ const colors =
41
+ palette === 'custom'
42
+ ? { ...DEFAULT_COLORS, ...customColors }
43
+ : { ...DEFAULT_COLORS, ...(SKIN_PRESETS[palette] || {}) }
44
+
45
+ const config = { colors, themes, switcher, storageKey: 'rokkit-theme' }
46
+
47
+ if (icons === 'custom' && iconPath) {
48
+ config.icons = { custom: iconPath }
49
+ }
50
+
51
+ return config
52
+ }
53
+
54
+ /**
55
+ * Generate the content for uno.config.js.
56
+ * @returns {string}
57
+ */
58
+ export function generateUnoConfig() {
59
+ return `import { defineConfig } from 'unocss'
60
+ import { presetRokkit } from '@rokkit/unocss'
61
+
62
+ export default defineConfig({
63
+ presets: [presetRokkit()]
64
+ })
65
+ `
66
+ }
67
+
68
+ /**
69
+ * Generate an array of CSS @import lines for app.css.
70
+ * @param {string[]} themes
71
+ * @returns {string[]}
72
+ */
73
+ export function generateAppCssImports(themes) {
74
+ const lines = [
75
+ "@import '@unocss/reset/tailwind.css';",
76
+ "@import '@rokkit/themes/dist/base';"
77
+ ]
78
+ for (const theme of themes) {
79
+ lines.push(`@import '@rokkit/themes/dist/${theme}';`)
80
+ }
81
+ return lines
82
+ }
83
+
84
+ /**
85
+ * Generate the flash-prevention script for app.html.
86
+ * Returns null when no script is needed (system switcher).
87
+ * @param {string} switcher — 'system' | 'manual' | 'full'
88
+ * @param {string} [storageKey='rokkit-theme']
89
+ * @returns {string | null}
90
+ */
91
+ export function generateInitScript(switcher, storageKey = 'rokkit-theme') {
92
+ if (switcher === 'system') return null
93
+
94
+ const setStyle =
95
+ switcher === 'full'
96
+ ? `b.dataset.style = t.style || 'rokkit'\n `
97
+ : ''
98
+
99
+ return ` <script>
100
+ (function () {
101
+ try {
102
+ var t = JSON.parse(localStorage.getItem('${storageKey}') || '{}')
103
+ var b = document.body
104
+ ${setStyle}b.dataset.mode = t.mode || 'dark'
105
+ } catch (e) {}
106
+ })()
107
+ </script>`
108
+ }
109
+
110
+ /**
111
+ * Interactive init command — prompts the user, writes config files.
112
+ */
113
+ export async function init() {
114
+ console.info('Rokkit Init — Setting up your SvelteKit project\n')
115
+
116
+ const response = await prompts([
117
+ {
118
+ type: 'select',
119
+ name: 'palette',
120
+ message: 'Color palette',
121
+ choices: [
122
+ { title: 'Default (orange/pink/sky)', value: 'default' },
123
+ { title: 'Vibrant (blue/purple/sky)', value: 'vibrant' },
124
+ { title: 'Seaweed (sky/green/blue)', value: 'seaweed' },
125
+ { title: 'Custom', value: 'custom' }
126
+ ]
127
+ },
128
+ {
129
+ type: (prev) => (prev === 'custom' ? 'text' : null),
130
+ name: 'primary',
131
+ message: 'Primary color (Tailwind palette name)',
132
+ initial: 'orange'
133
+ },
134
+ {
135
+ type: (_, values) => (values.palette === 'custom' ? 'text' : null),
136
+ name: 'secondary',
137
+ message: 'Secondary color',
138
+ initial: 'pink'
139
+ },
140
+ {
141
+ type: (_, values) => (values.palette === 'custom' ? 'text' : null),
142
+ name: 'accent',
143
+ message: 'Accent color',
144
+ initial: 'sky'
145
+ },
146
+ {
147
+ type: (_, values) => (values.palette === 'custom' ? 'text' : null),
148
+ name: 'surface',
149
+ message: 'Surface color',
150
+ initial: 'slate'
151
+ },
152
+ {
153
+ type: 'select',
154
+ name: 'icons',
155
+ message: 'Icon collections',
156
+ choices: [
157
+ { title: 'Rokkit icons only', value: 'rokkit' },
158
+ { title: 'Rokkit + custom collection', value: 'custom' }
159
+ ]
160
+ },
161
+ {
162
+ type: (prev) => (prev === 'custom' ? 'text' : null),
163
+ name: 'iconPath',
164
+ message: 'Path to custom icon collection JSON',
165
+ initial: './static/icons/custom.json'
166
+ },
167
+ {
168
+ type: 'multiselect',
169
+ name: 'themes',
170
+ message: 'Theme styles',
171
+ choices: [
172
+ { title: 'Rokkit', value: 'rokkit', selected: true },
173
+ { title: 'Minimal', value: 'minimal' },
174
+ { title: 'Material', value: 'material' }
175
+ ],
176
+ min: 1
177
+ },
178
+ {
179
+ type: 'select',
180
+ name: 'switcher',
181
+ message: 'Theme switching',
182
+ choices: [
183
+ { title: 'System only (prefers-color-scheme)', value: 'system' },
184
+ { title: 'Manual (light/dark toggle)', value: 'manual' },
185
+ { title: 'Full (light/dark + style variants)', value: 'full' }
186
+ ]
187
+ }
188
+ ])
189
+
190
+ if (response.palette === 'custom') {
191
+ response.customColors = {
192
+ primary: response.primary,
193
+ secondary: response.secondary,
194
+ accent: response.accent,
195
+ surface: response.surface
196
+ }
197
+ }
198
+
199
+ const config = generateConfig(response)
200
+ const cwd = process.cwd()
201
+
202
+ // --- rokkit.config.js ---
203
+ const configPath = resolve(cwd, 'rokkit.config.js')
204
+ if (existsSync(configPath)) {
205
+ console.warn(' rokkit.config.js already exists — skipping')
206
+ } else {
207
+ writeFileSync(configPath, `export default ${JSON.stringify(config, null, 2)}\n`)
208
+ console.info(' Created rokkit.config.js')
209
+ }
210
+
211
+ // --- uno.config.js ---
212
+ const unoPath = resolve(cwd, 'uno.config.js')
213
+ if (existsSync(unoPath)) {
214
+ console.warn(' uno.config.js already exists — skipping (see rokkit doctor for migration)')
215
+ } else {
216
+ writeFileSync(unoPath, generateUnoConfig())
217
+ console.info(' Created uno.config.js')
218
+ }
219
+
220
+ // --- src/app.css ---
221
+ const appCssPath = resolve(cwd, 'src/app.css')
222
+ const cssImports = generateAppCssImports(config.themes)
223
+ if (existsSync(appCssPath)) {
224
+ const existing = readFileSync(appCssPath, 'utf-8')
225
+ const missing = cssImports.filter((line) => !existing.includes(line))
226
+ if (missing.length > 0) {
227
+ writeFileSync(appCssPath, `${missing.join('\n') }\n${ existing}`)
228
+ console.info(` Patched app.css — added ${missing.length} imports`)
229
+ } else {
230
+ console.info(' app.css already has theme imports')
231
+ }
232
+ } else {
233
+ writeFileSync(appCssPath, `${cssImports.join('\n') }\n`)
234
+ console.info(' Created src/app.css')
235
+ }
236
+
237
+ // --- src/app.html (theme init script) ---
238
+ const initScript = generateInitScript(config.switcher, config.storageKey)
239
+ if (initScript) {
240
+ const appHtmlPath = resolve(cwd, 'src/app.html')
241
+ if (existsSync(appHtmlPath)) {
242
+ const html = readFileSync(appHtmlPath, 'utf-8')
243
+ if (!html.includes('rokkit-theme') && !html.includes(config.storageKey)) {
244
+ const patched = html.replace(/(<body[^>]*>)/, `$1\n${initScript}`)
245
+ writeFileSync(appHtmlPath, patched)
246
+ console.info(' Patched app.html — added theme init script')
247
+ } else {
248
+ console.info(' app.html already has init script')
249
+ }
250
+ }
251
+ }
252
+
253
+ console.info('\nDone! Run `rokkit doctor` to verify your setup.')
254
+ }